@squiz/formatted-text-editor 1.40.1-alpha.7 → 1.41.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.
Files changed (56) hide show
  1. package/demo/AppContext.tsx +14 -3
  2. package/demo/resources.json +135 -3
  3. package/lib/Editor/Editor.js +3 -2
  4. package/lib/EditorToolbar/Toolbar.js +2 -0
  5. package/lib/EditorToolbar/Tools/Image/ImageButton.js +1 -1
  6. package/lib/EditorToolbar/Tools/Lists/ListButtons.d.ts +2 -0
  7. package/lib/EditorToolbar/Tools/Lists/ListButtons.js +14 -0
  8. package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.d.ts +2 -0
  9. package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.js +22 -0
  10. package/lib/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.d.ts +2 -0
  11. package/lib/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.js +22 -0
  12. package/lib/Extensions/Extensions.d.ts +2 -1
  13. package/lib/Extensions/Extensions.js +9 -1
  14. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +14 -0
  15. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +63 -0
  16. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +1 -0
  17. package/lib/Extensions/ImageExtension/AssetImageExtension.js +4 -4
  18. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +2 -0
  19. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +4 -4
  20. package/lib/hooks/useFocus.js +24 -5
  21. package/lib/index.css +13 -0
  22. package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +1 -0
  23. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +1 -0
  24. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +3 -0
  25. package/package.json +4 -4
  26. package/src/Editor/Editor.spec.tsx +91 -16
  27. package/src/Editor/Editor.tsx +3 -2
  28. package/src/EditorToolbar/Toolbar.tsx +2 -0
  29. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +12 -2
  30. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +1 -1
  31. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +14 -2
  32. package/src/EditorToolbar/Tools/Lists/ListButtons.tsx +14 -0
  33. package/src/EditorToolbar/Tools/Lists/OrderedList/OrderListButton.spec.tsx +39 -0
  34. package/src/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.tsx +30 -0
  35. package/src/EditorToolbar/Tools/Lists/UnorderedList/UnorderedList.spec.tsx +19 -0
  36. package/src/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.tsx +30 -0
  37. package/src/Extensions/Extensions.ts +14 -2
  38. package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +73 -0
  39. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +2 -1
  40. package/src/Extensions/ImageExtension/AssetImageExtension.ts +5 -5
  41. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +3 -1
  42. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +6 -5
  43. package/src/hooks/useFocus.ts +30 -7
  44. package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +1 -0
  45. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +2 -0
  46. package/src/ui/_typography.scss +16 -0
  47. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +191 -0
  48. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +3 -0
  49. package/src/utils/getNodeNamesByGroup.spec.ts +11 -1
  50. package/tests/mockResourceBrowserContext.tsx +2 -2
  51. package/tests/renderWithContext.tsx +30 -1
  52. package/tests/renderWithEditor.tsx +18 -13
  53. package/lib/utils/resolveMatrixAssetUrl.d.ts +0 -1
  54. package/lib/utils/resolveMatrixAssetUrl.js +0 -10
  55. package/src/utils/resolveMatrixAssetUrl.spec.ts +0 -26
  56. package/src/utils/resolveMatrixAssetUrl.ts +0 -7
@@ -1,27 +1,16 @@
1
+ import '@testing-library/jest-dom';
1
2
  import React from 'react';
2
- import { act, fireEvent, render, screen } from '@testing-library/react';
3
+ import { act, fireEvent, render, screen, within } from '@testing-library/react';
4
+ import { ResourceBrowserContext } from '@squiz/resource-browser';
3
5
  import Editor from './Editor';
4
- import '@testing-library/jest-dom';
5
6
  import { renderWithEditor } from '../../tests';
6
7
  import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
8
+ import * as useFocus from '../hooks/useFocus';
7
9
 
8
- const isVisible = jest.fn().mockReturnValue(false);
9
10
  const handleFocusMock = jest.fn();
10
11
  const handleBlurMock = jest.fn();
11
- jest.mock('../hooks/useFocus', () => ({
12
- __esModule: true,
13
- default: () => ({
14
- isVisible: isVisible(),
15
- handleFocus: handleFocusMock,
16
- handleBlur: handleBlurMock,
17
- }),
18
- }));
19
12
 
20
13
  describe('Formatted text editor', () => {
21
- afterEach(() => {
22
- jest.clearAllMocks();
23
- });
24
-
25
14
  it('Renders the text editor', () => {
26
15
  render(<Editor />);
27
16
  expect(screen.getByRole('textbox')).toBeInTheDocument();
@@ -329,6 +318,13 @@ describe('Formatted text editor', () => {
329
318
  });
330
319
 
331
320
  it('triggers handleFocus when editor is focused', async () => {
321
+ jest.spyOn(useFocus, 'default').mockReturnValue({
322
+ isVisible: false,
323
+ handleFocus: handleFocusMock,
324
+ handleBlur: handleBlurMock,
325
+ wrapperRef: { current: null },
326
+ });
327
+
332
328
  const { getByLabelText } = render(<Editor />);
333
329
  const editorInput = getByLabelText('Text editor');
334
330
 
@@ -339,6 +335,13 @@ describe('Formatted text editor', () => {
339
335
  });
340
336
 
341
337
  it('triggers handleBlur when editor is blurred', () => {
338
+ jest.spyOn(useFocus, 'default').mockReturnValue({
339
+ isVisible: false,
340
+ handleFocus: handleFocusMock,
341
+ handleBlur: handleBlurMock,
342
+ wrapperRef: { current: null },
343
+ });
344
+
342
345
  const { getByLabelText } = render(<Editor />);
343
346
  const editorInput = getByLabelText('Text editor');
344
347
 
@@ -349,9 +352,81 @@ describe('Formatted text editor', () => {
349
352
  });
350
353
 
351
354
  it('should apply hide class when focus hook returns false', () => {
352
- isVisible.mockReturnValue(true);
355
+ jest.spyOn(useFocus, 'default').mockReturnValue({
356
+ isVisible: true,
357
+ handleFocus: handleFocusMock,
358
+ handleBlur: handleBlurMock,
359
+ wrapperRef: { current: null },
360
+ });
361
+
353
362
  const { container } = render(<Editor />);
354
363
 
355
364
  expect(container.querySelector('.show-toolbar')).toBeInTheDocument();
356
365
  });
366
+
367
+ it('Interactivity in modals functions correctly, toolbar remains visible throughout', async () => {
368
+ const onRequestSources = jest.fn().mockResolvedValue([
369
+ {
370
+ id: 'my-matrix-instance',
371
+ name: 'My Matrix instance',
372
+ nodes: [],
373
+ },
374
+ ]);
375
+ const onRequestChildren = jest
376
+ .fn()
377
+ .mockResolvedValueOnce([
378
+ {
379
+ id: '1',
380
+ name: 'Folder 1',
381
+ type: { code: 'folder', name: 'Folder' },
382
+ status: { code: 'live', name: 'Live' },
383
+ childCount: 1,
384
+ },
385
+ ])
386
+ .mockResolvedValueOnce([
387
+ {
388
+ id: '2',
389
+ name: 'Folder 2',
390
+ type: { code: 'folder', name: 'Folder' },
391
+ status: { code: 'live', name: 'Live' },
392
+ childCount: 1,
393
+ },
394
+ ])
395
+ .mockResolvedValueOnce([
396
+ {
397
+ id: '3',
398
+ name: 'My image',
399
+ type: { code: 'image', name: 'Image' },
400
+ status: { code: 'live', name: 'Live' },
401
+ childCount: 0,
402
+ },
403
+ ]);
404
+ const onRequestResource = jest.fn().mockResolvedValue({
405
+ id: '3',
406
+ name: 'My image',
407
+ type: { code: 'image', name: 'Image' },
408
+ status: { code: 'live', name: 'Live' },
409
+ childCount: '1',
410
+ });
411
+
412
+ render(
413
+ <ResourceBrowserContext.Provider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
414
+ <Editor />
415
+ </ResourceBrowserContext.Provider>,
416
+ );
417
+
418
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Link (cmd+K)' })));
419
+ await act(() => fireEvent.click(screen.getByText('Choose asset')));
420
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to My Matrix instance children' })));
421
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to Folder 1 children' })));
422
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to Folder 2 children' })));
423
+ await act(() => fireEvent.blur(screen.getByRole('button', { name: /My image/ })));
424
+
425
+ // Contents of "Folder 2" should be mounted.
426
+ // Toolbar (Link button) should be mounted.
427
+ // Toolbar should be shown (.show-toolbar class).
428
+ expect(screen.getByRole('button', { name: /My image/ })).toBeInTheDocument();
429
+ expect(within(document.body).getByRole('button', { name: 'Link (cmd+K)', hidden: true })).toBeInTheDocument();
430
+ expect(document.querySelector('.show-toolbar')).toBeInTheDocument();
431
+ });
357
432
  });
@@ -7,6 +7,7 @@ import { Toolbar, FloatingToolbar } from '../EditorToolbar';
7
7
  import { EditorContext } from './EditorContext';
8
8
  import { createExtensions } from '../Extensions/Extensions';
9
9
  import useFocus from '../hooks/useFocus';
10
+ import { ResourceBrowserContext } from '@squiz/resource-browser';
10
11
 
11
12
  type EditorProps = {
12
13
  content?: RemirrorContentType;
@@ -34,7 +35,7 @@ const WrappedEditor = () => {
34
35
 
35
36
  const Editor = ({ content, editable = true, onChange, children, isFocused }: EditorProps) => {
36
37
  const { manager, state, setState } = useRemirror({
37
- extensions: createExtensions(useContext(EditorContext)),
38
+ extensions: createExtensions(useContext(EditorContext), useContext(ResourceBrowserContext)),
38
39
  content,
39
40
  selection: 'start',
40
41
  stringHandler: 'html',
@@ -60,7 +61,7 @@ const Editor = ({ content, editable = true, onChange, children, isFocused }: Edi
60
61
  ref={wrapperRef}
61
62
  onBlur={handleBlur}
62
63
  onFocusCapture={handleFocus}
63
- className={clsx('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled')}
64
+ className={clsx('formatted-text-editor', !editable && 'formatted-text-editor--is-disabled')}
64
65
  >
65
66
  <Remirror
66
67
  manager={manager}
@@ -12,6 +12,7 @@ import LinkButton from './Tools/Link/LinkButton';
12
12
  import ImageButton from './Tools/Image/ImageButton';
13
13
  import RemoveLinkButton from './Tools/Link/RemoveLinkButton';
14
14
  import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton';
15
+ import ListButtons from './Tools/Lists/ListButtons';
15
16
  import { useExtensionNames } from '../hooks';
16
17
 
17
18
  type ToolbarProps = {
@@ -38,6 +39,7 @@ export const Toolbar = ({ isVisible }: ToolbarProps) => {
38
39
  {extensionNames.italic && <ItalicButton />}
39
40
  {extensionNames.underline && <UnderlineButton />}
40
41
  {extensionNames.nodeFormatting && <TextAlignButtons />}
42
+ {extensionNames.listItem && <ListButtons />}
41
43
  {extensionNames.link && (
42
44
  <>
43
45
  <LinkButton />
@@ -280,7 +280,12 @@ describe('ImageButton', () => {
280
280
  content: [
281
281
  {
282
282
  type: 'assetImage',
283
- attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
283
+ attrs: {
284
+ matrixAssetId: 'image-resource-id',
285
+ matrixIdentifier,
286
+ matrixDomain,
287
+ src: 'https://default-resource/',
288
+ },
284
289
  },
285
290
  { type: 'text', text: 'Some nonsense content here' },
286
291
  ],
@@ -339,7 +344,12 @@ describe('ImageButton', () => {
339
344
  },
340
345
  {
341
346
  type: 'assetImage',
342
- attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
347
+ attrs: {
348
+ matrixAssetId: 'image-resource-id',
349
+ matrixIdentifier,
350
+ matrixDomain,
351
+ src: 'https://default-resource/',
352
+ },
343
353
  },
344
354
  { type: 'text', text: ' nonsense' },
345
355
  ],
@@ -36,7 +36,7 @@ const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
36
36
  }
37
37
  };
38
38
 
39
- const handleSubmit = (data: ImageFormData) => {
39
+ const handleSubmit = async (data: ImageFormData) => {
40
40
  insertImageFromData(data);
41
41
  setShowModal(false);
42
42
  };
@@ -274,7 +274,13 @@ describe('LinkButton', () => {
274
274
  marks: [
275
275
  {
276
276
  type: 'assetLink',
277
- attrs: { matrixAssetId: 'my-resource-id', target: '_self', matrixDomain, matrixIdentifier },
277
+ attrs: {
278
+ matrixAssetId: 'my-resource-id',
279
+ target: '_self',
280
+ matrixDomain,
281
+ matrixIdentifier,
282
+ href: 'https://default-resource/',
283
+ },
278
284
  },
279
285
  ],
280
286
  },
@@ -328,7 +334,13 @@ describe('LinkButton', () => {
328
334
  marks: [
329
335
  {
330
336
  type: 'assetLink',
331
- attrs: { matrixAssetId: 'my-resource-id', target: '_blank', matrixDomain, matrixIdentifier },
337
+ attrs: {
338
+ matrixAssetId: 'my-resource-id',
339
+ target: '_blank',
340
+ matrixDomain,
341
+ matrixIdentifier,
342
+ href: 'https://default-resource/',
343
+ },
332
344
  },
333
345
  ],
334
346
  },
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import OrderedListButton from './OrderedList/OrderedListButton';
3
+ import UnorderedListButton from './UnorderedList/UnorderedListButton';
4
+ import { VerticalDivider } from '@remirror/react-components';
5
+
6
+ const ListButtons = () => (
7
+ <>
8
+ <UnorderedListButton />
9
+ <OrderedListButton />
10
+ <VerticalDivider />
11
+ </>
12
+ );
13
+
14
+ export default ListButtons;
@@ -0,0 +1,39 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import Editor from '../../../../Editor/Editor';
4
+ import React from 'react';
5
+
6
+ describe('Ordered list button', () => {
7
+ it('Renders the ordered list button', () => {
8
+ const { baseElement, getByRole } = render(<Editor />);
9
+ expect(baseElement).toBeTruthy();
10
+ expect(getByRole('button', { name: 'Ordered list' })).toBeTruthy();
11
+ });
12
+
13
+ it('Applies ordered list after selecting ordered list button', () => {
14
+ const { baseElement, getByRole } = render(<Editor />);
15
+ expect(baseElement).toBeTruthy();
16
+
17
+ const OrderedListButton = getByRole('button', { name: 'Ordered list' });
18
+ expect(OrderedListButton).toBeTruthy();
19
+ expect(OrderedListButton.classList.contains('is-active')).toBeFalsy();
20
+
21
+ fireEvent.click(OrderedListButton);
22
+
23
+ setTimeout(() => {
24
+ expect(OrderedListButton.classList.contains('is-active')).toBeTruthy();
25
+ }, 50);
26
+ });
27
+
28
+ it('Should apply order list to editor after clicking button', () => {
29
+ const { baseElement, getByRole } = render(<Editor />);
30
+ expect(baseElement).toBeTruthy();
31
+ expect(baseElement.querySelector('ol')).toBeFalsy();
32
+
33
+ const OrderedListButton = getByRole('button', { name: 'Ordered list' });
34
+ expect(OrderedListButton).toBeTruthy();
35
+ fireEvent.click(OrderedListButton);
36
+
37
+ expect(baseElement.querySelector('ol')).toBeTruthy();
38
+ });
39
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
+ import { OrderedListExtension } from '@remirror/extension-list';
4
+ import Button from '../../../../ui/Button/Button';
5
+ import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
6
+
7
+ const OrderedListButton = () => {
8
+ const { toggleOrderedList } = useCommands();
9
+ const chain = useChainedCommands();
10
+
11
+ const active = useActive<OrderedListExtension>();
12
+ const enabled = toggleOrderedList.enabled();
13
+ const handleSelect = () => {
14
+ if (toggleOrderedList.enabled()) {
15
+ chain.toggleOrderedList().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <Button
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.orderedList()}
24
+ icon={<FormatListNumberedIcon />}
25
+ label="Ordered list"
26
+ />
27
+ );
28
+ };
29
+
30
+ export default OrderedListButton;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import Editor from '../../../../Editor/Editor';
5
+
6
+ describe('Unordered list button', () => {
7
+ it('Renders the unordered list button', () => {
8
+ render(<Editor />);
9
+ expect(screen.getByRole('button', { name: 'Unordered list' })).toBeInTheDocument();
10
+ });
11
+
12
+ it('Activates the button if clicked', () => {
13
+ render(<Editor />);
14
+ expect(screen.getByRole('button', { name: 'Unordered list' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
+ const ul = screen.getByRole('button', { name: 'Unordered list' });
16
+ fireEvent.click(ul);
17
+ expect(ul.classList.contains('squiz-fte-btn--is-active')).toBeTruthy();
18
+ });
19
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
+ import { BulletListExtension } from 'remirror/extensions';
4
+ import Button from '../../../../ui/Button/Button';
5
+ import FormatListBulletedRoundedIcon from '@mui/icons-material/FormatListBulletedRounded';
6
+
7
+ const UnorderedListButton = () => {
8
+ const { toggleBulletList } = useCommands();
9
+ const chain = useChainedCommands();
10
+
11
+ const active = useActive<BulletListExtension>();
12
+ const enabled = toggleBulletList.enabled();
13
+ const handleSelect = () => {
14
+ if (toggleBulletList.enabled()) {
15
+ chain.toggleBulletList().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <Button
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.bulletList()}
24
+ icon={<FormatListBulletedRoundedIcon />}
25
+ label="Unordered list"
26
+ />
27
+ );
28
+ };
29
+
30
+ export default UnorderedListButton;
@@ -6,6 +6,10 @@ import {
6
6
  ParagraphExtension,
7
7
  UnderlineExtension,
8
8
  HistoryExtension,
9
+ OrderedListExtension,
10
+ BulletListExtension,
11
+ ListItemExtension,
12
+ PlaceholderExtension,
9
13
  } from 'remirror/extensions';
10
14
  import { Extension } from '@remirror/core';
11
15
  import { PreformattedExtension } from './PreformattedExtension/PreformattedExtension';
@@ -18,7 +22,8 @@ import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
18
22
  import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
19
23
  import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
20
24
  import { UnsupportedNodeExtension } from './UnsuportedExtension/UnsupportedNodeExtension';
21
-
25
+ import { ResourceBrowserContextProps } from '@squiz/resource-browser';
26
+ import { FetchUrlExtension } from './FetchUrlExtension/FetchUrlExtension';
22
27
  export enum NodeName {
23
28
  Image = 'image',
24
29
  CodeBlock = 'codeBlock',
@@ -32,7 +37,7 @@ export enum MarkName {
32
37
  AssetLink = 'assetLink',
33
38
  }
34
39
 
35
- export const createExtensions = (context: EditorContextOptions) => {
40
+ export const createExtensions = (context: EditorContextOptions, browserContext: ResourceBrowserContextProps) => {
36
41
  return (): Extension[] => {
37
42
  return [
38
43
  new CommandsExtension(),
@@ -56,6 +61,13 @@ export const createExtensions = (context: EditorContextOptions) => {
56
61
  }),
57
62
  new UnsupportedNodeExtension(),
58
63
  new ClearFormattingExtension(),
64
+ new BulletListExtension(),
65
+ new ListItemExtension(),
66
+ new OrderedListExtension(),
67
+ new PlaceholderExtension(),
68
+ new FetchUrlExtension({
69
+ fetchUrl: browserContext.onRequestResource,
70
+ }),
59
71
  ];
60
72
  };
61
73
  };
@@ -0,0 +1,73 @@
1
+ import { extension, PlainExtension, Mark } from '@remirror/core';
2
+ import { Dispose, EditorView } from '@remirror/core-types';
3
+ import { Resource } from '@squiz/resource-browser';
4
+ import { MarkName, NodeName } from '../Extensions';
5
+
6
+ export type FetchUrlOptions = {
7
+ fetchUrl?: (params: { resource: string; source: string }) => Promise<any>;
8
+ };
9
+
10
+ @extension<FetchUrlOptions>({
11
+ defaultOptions: {
12
+ fetchUrl: () => Promise.resolve(),
13
+ },
14
+ })
15
+ export class FetchUrlExtension extends PlainExtension<FetchUrlOptions> {
16
+ get name() {
17
+ return 'fetchUrl';
18
+ }
19
+
20
+ // Implement the `onView` lifecycle method to scan for `[fetch]` and replace attributes
21
+ onView(view: EditorView): Dispose | void {
22
+ const { state } = view;
23
+ const { tr } = state;
24
+
25
+ const promises: Promise<void>[] = [];
26
+
27
+ state.doc.descendants((node, pos) => {
28
+ if (node.type.name === NodeName.AssetImage && node.attrs.src === '') {
29
+ promises.push(
30
+ this.fetchAndReplace(node.attrs.matrixAssetId, node.attrs.matrixIdentifier, (url: string) => {
31
+ const newNode = state.schema.nodes[NodeName.AssetImage].create({ ...node.attrs, src: url });
32
+ tr.replaceWith(pos, pos + node.nodeSize, newNode);
33
+ }),
34
+ );
35
+ }
36
+
37
+ const assetLinkMark = this.findAssetLinkMark(node.marks as Mark[]);
38
+ if (node.type.name === 'text' && assetLinkMark) {
39
+ promises.push(
40
+ this.fetchAndReplace(
41
+ assetLinkMark.attrs.matrixAssetId,
42
+ assetLinkMark.attrs.matrixIdentifier,
43
+ (url: string) => {
44
+ const updatedMark = assetLinkMark.type.create({ ...assetLinkMark.attrs, href: url });
45
+ tr.addMark(pos, pos + node.nodeSize, updatedMark);
46
+ },
47
+ ),
48
+ );
49
+ }
50
+ });
51
+
52
+ if (promises.length) {
53
+ Promise.all(promises).then(() => {
54
+ view.dispatch(tr);
55
+ });
56
+ }
57
+ }
58
+
59
+ private findAssetLinkMark(marks: Mark[]): Mark | undefined {
60
+ return marks.find((mark) => mark.type.name === MarkName.AssetLink && mark.attrs.href === '');
61
+ }
62
+
63
+ private fetchAndReplace(resource: string, source: string, onFetched: (url: string) => void): Promise<void> {
64
+ return this.options
65
+ .fetchUrl({ resource, source })
66
+ .then((asset: Resource) => {
67
+ onFetched(asset.url);
68
+ })
69
+ .catch((error) => {
70
+ console.error('Error fetching URL:', error);
71
+ });
72
+ }
73
+ }
@@ -20,6 +20,7 @@ describe('AssetImageExtension', () => {
20
20
  matrixAssetId: '123',
21
21
  matrixDomain: 'https://matrix-domain.squiz.net',
22
22
  matrixIdentifier: 'matrix-api-identifier',
23
+ src: 'https://default-resource/',
23
24
  },
24
25
  },
25
26
  ],
@@ -64,7 +65,7 @@ describe('AssetImageExtension', () => {
64
65
 
65
66
  expect(getHtmlContent()).toEqual(
66
67
  '<img ' +
67
- 'src="https://matrix-domain.squiz.net/_nocache?a=123" ' +
68
+ 'src="https://default-resource/" ' +
68
69
  'data-matrix-asset-id="123" ' +
69
70
  'data-matrix-identifier="matrix-api-identifier" ' +
70
71
  'data-matrix-domain="https://matrix-domain.squiz.net" ' +
@@ -12,7 +12,6 @@ import {
12
12
  CommandFunction,
13
13
  } from '@remirror/core';
14
14
  import { getTextSelection } from 'remirror';
15
- import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
16
15
  import { NodeName } from '../Extensions';
17
16
 
18
17
  export type AssetImageOptions = {
@@ -23,6 +22,7 @@ export type AssetImageAttributes = {
23
22
  matrixAssetId: string;
24
23
  matrixIdentifier: string;
25
24
  matrixDomain: string;
25
+ url: string;
26
26
  };
27
27
 
28
28
  @extension<AssetImageOptions>({
@@ -51,6 +51,7 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
51
51
  matrixAssetId: {},
52
52
  matrixIdentifier: {},
53
53
  matrixDomain: { default: this.options.matrixDomain },
54
+ src: { default: '' },
54
55
  },
55
56
  parseDOM: [
56
57
  {
@@ -78,14 +79,13 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
78
79
  },
79
80
  ],
80
81
  toDOM: (node) => {
81
- const { matrixAssetId, matrixIdentifier, matrixDomain, ...rest } = omitExtraAttributes(node.attrs, extra);
82
-
82
+ const { matrixAssetId, matrixIdentifier, matrixDomain, src, ...rest } = omitExtraAttributes(node.attrs, extra);
83
83
  return [
84
84
  'img',
85
85
  {
86
86
  ...extra.dom(node),
87
87
  ...rest,
88
- src: resolveMatrixAssetUrl(String(matrixAssetId), String(matrixDomain)),
88
+ src,
89
89
  'data-matrix-asset-id': matrixAssetId,
90
90
  'data-matrix-identifier': matrixIdentifier,
91
91
  'data-matrix-domain': matrixDomain,
@@ -99,7 +99,7 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
99
99
  insertAssetImage(attrs: AssetImageAttributes): CommandFunction {
100
100
  return ({ tr, dispatch }) => {
101
101
  const { from, to } = getTextSelection(tr.selection, tr.doc);
102
- const node = this.type.create(attrs);
102
+ const node = this.type.create({ ...attrs, src: attrs.url });
103
103
 
104
104
  dispatch?.(tr.replaceRangeWith(from, to, node));
105
105
 
@@ -23,6 +23,7 @@ describe('AssetLinkExtension', () => {
23
23
  {
24
24
  type: 'assetLink',
25
25
  attrs: {
26
+ href: 'https://default-resource/',
26
27
  matrixAssetId: '123',
27
28
  matrixDomain: 'https://matrix-domain.squiz.net',
28
29
  matrixIdentifier: 'matrix-api-identifier',
@@ -78,6 +79,7 @@ describe('AssetLinkExtension', () => {
78
79
  {
79
80
  type: 'assetLink',
80
81
  attrs: {
82
+ href: 'https://default-resource/',
81
83
  matrixAssetId: '123',
82
84
  matrixDomain: 'https://matrix-domain.squiz.net',
83
85
  matrixIdentifier: 'matrix-api-identifier',
@@ -92,7 +94,7 @@ describe('AssetLinkExtension', () => {
92
94
 
93
95
  expect(getHtmlContent()).toEqual(
94
96
  '<a rel="noopener noreferrer nofollow" ' +
95
- 'href="https://matrix-domain.squiz.net/_nocache?a=123" ' +
97
+ 'href="https://default-resource/" ' +
96
98
  'target="_blank" ' +
97
99
  'data-matrix-asset-id="123" ' +
98
100
  'data-matrix-identifier="matrix-api-identifier" ' +
@@ -10,13 +10,14 @@ import { command, CommandFunction, extension, MarkExtension, omitExtraAttributes
10
10
  import { LinkTarget, validateTarget } from './common';
11
11
  import { CommandsExtension } from '../CommandsExtension/CommandsExtension';
12
12
  import { MarkName } from '../Extensions';
13
- import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
14
13
 
15
14
  export type AssetLinkAttributes = {
16
15
  matrixAssetId: string;
17
16
  matrixIdentifier: string;
18
17
  matrixDomain: string;
19
18
  target: LinkTarget;
19
+ href: string;
20
+ url: string;
20
21
  };
21
22
 
22
23
  export type AssetLinkOptions = {
@@ -53,6 +54,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
53
54
  ...extra.defaults(),
54
55
  matrixAssetId: {},
55
56
  matrixIdentifier: {},
57
+ href: { default: '' },
56
58
  matrixDomain: { default: this.options.matrixDomain },
57
59
  target: { default: this.options.defaultTarget },
58
60
  },
@@ -87,7 +89,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
87
89
  },
88
90
  ],
89
91
  toDOM: (node) => {
90
- const { matrixAssetId, matrixIdentifier, matrixDomain, target, ...rest } = omitExtraAttributes(
92
+ const { matrixAssetId, matrixIdentifier, matrixDomain, target, href, ...rest } = omitExtraAttributes(
91
93
  node.attrs,
92
94
  extra,
93
95
  );
@@ -96,7 +98,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
96
98
  ...extra.dom(node),
97
99
  ...rest,
98
100
  rel,
99
- href: resolveMatrixAssetUrl(String(matrixAssetId), String(matrixDomain)),
101
+ href,
100
102
  target: validateTarget(target, this.options.supportedTargets, this.options.defaultTarget),
101
103
  'data-matrix-asset-id': matrixAssetId,
102
104
  'data-matrix-identifier': matrixIdentifier,
@@ -111,14 +113,13 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
111
113
  @command()
112
114
  updateAssetLink({ text, attrs, range }: UpdateAssetLinkProps): CommandFunction {
113
115
  return this.store.getExtension(CommandsExtension).updateMark({
114
- attrs,
116
+ attrs: { ...attrs, href: attrs.url },
115
117
  text,
116
118
  range,
117
119
  mark: this.type,
118
120
  removeMark: !attrs.matrixAssetId,
119
121
  });
120
122
  }
121
-
122
123
  @command()
123
124
  removeAssetLink(): CommandFunction {
124
125
  return removeMark({ type: this.type });