@squiz/formatted-text-editor 2.6.4 → 2.6.6

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 (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/demo/diff/AppContext.tsx +36 -2
  3. package/demo/diff/contentApi.json +21 -0
  4. package/demo/diff/index.scss +2 -0
  5. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +3 -2
  6. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -4
  7. package/lib/EditorToolbar/Tools/Link/LinkButton.js +11 -7
  8. package/lib/EditorToolbar/Tools/Link/LinkModal.js +8 -4
  9. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +2 -2
  10. package/lib/Extensions/Extensions.d.ts +2 -1
  11. package/lib/Extensions/Extensions.js +3 -0
  12. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +1 -0
  13. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +10 -0
  14. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +1 -1
  15. package/lib/Extensions/LinkExtension/DamLinkExtension.d.ts +29 -0
  16. package/lib/Extensions/LinkExtension/DamLinkExtension.js +111 -0
  17. package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
  18. package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +16 -2
  19. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +71 -20
  20. package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.d.ts +3 -0
  21. package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.js +8 -0
  22. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +15 -1
  23. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +13 -0
  24. package/package.json +9 -9
  25. package/src/Editor/Editor.spec.tsx +1 -0
  26. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +26 -7
  27. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +74 -1
  28. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +19 -8
  29. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +10 -5
  30. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +4 -3
  31. package/src/Extensions/Extensions.ts +3 -0
  32. package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +14 -0
  33. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +1 -1
  34. package/src/Extensions/LinkExtension/DamLinkExtension.spec.ts +110 -0
  35. package/src/Extensions/LinkExtension/DamLinkExtension.ts +137 -0
  36. package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
  37. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +83 -26
  38. package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.spec.tsx +71 -1
  39. package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.tsx +30 -16
  40. package/src/utils/converters/mocks/squizNodeJson.mock.ts +48 -0
  41. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +63 -0
  42. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +16 -1
  43. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +12 -0
  44. package/src/utils/getMarkNamesByGroup.spec.ts +1 -1
  45. package/tests/renderWithContext.tsx +2 -0
@@ -40,6 +40,7 @@ const ResourceBrowserSelector = ({ modalTitle, allowedTypes, value, onChange, ..
40
40
  return {
41
41
  sourceId: value.damSystemIdentifier,
42
42
  resourceId: value.damObjectId,
43
+ variant: value.damAdditional?.variant,
43
44
  };
44
45
  }
45
46
  return null;
@@ -53,6 +54,7 @@ const ResourceBrowserSelector = ({ modalTitle, allowedTypes, value, onChange, ..
53
54
  damSystemIdentifier: undefined,
54
55
  damObjectId: undefined,
55
56
  damSystemType: undefined,
57
+ damAdditional: undefined,
56
58
  url: undefined,
57
59
  };
58
60
  if (resource?.source?.type === 'matrix') {
@@ -65,11 +67,17 @@ const ResourceBrowserSelector = ({ modalTitle, allowedTypes, value, onChange, ..
65
67
  };
66
68
  }
67
69
  else if (resource?.source?.type === 'dam') {
70
+ const damResource = resource;
68
71
  onChangeData = {
69
72
  ...value,
70
73
  damSystemIdentifier: resource?.source?.id,
71
74
  damObjectId: resource?.id,
72
75
  damSystemType: (resource?.source).configuration.externalType,
76
+ damAdditional: damResource.variant
77
+ ? {
78
+ variant: damResource.variant,
79
+ }
80
+ : undefined,
73
81
  url: resource?.url,
74
82
  nodeType: Extensions_1.NodeName.DAMImage,
75
83
  };
@@ -115,12 +115,16 @@ const transformNode = (node) => {
115
115
  };
116
116
  }
117
117
  if (node.type.name === Extensions_1.NodeName.DAMImage) {
118
+ let damAdditional = node.attrs.damAdditional;
119
+ if (damAdditional && typeof node.attrs.damAdditional === 'string') {
120
+ damAdditional = JSON.parse(node.attrs.damAdditional);
121
+ }
118
122
  transformedNode = {
119
123
  type: 'dam-image',
120
124
  damObjectId: node.attrs.damObjectId,
121
125
  damSystemIdentifier: node.attrs.damSystemIdentifier,
122
126
  damSystemType: node.attrs.damSystemType,
123
- damAdditional: node.attrs.damAdditional ? JSON.parse(node.attrs.damAdditional) : undefined,
127
+ damAdditional,
124
128
  };
125
129
  }
126
130
  node.marks.forEach((mark) => {
@@ -212,6 +216,16 @@ const transformMark = (mark, node) => {
212
216
  matrixAssetId: mark.attrs.matrixAssetId,
213
217
  children: [],
214
218
  });
219
+ case 'DAMLink':
220
+ return wrapNodeIfNeeded(node, {
221
+ type: 'link-to-dam-asset',
222
+ target: mark.attrs.target,
223
+ damSystemType: mark.attrs.damSystemType,
224
+ damSystemIdentifier: mark.attrs.damSystemIdentifier,
225
+ damObjectId: mark.attrs.damObjectId,
226
+ damAdditional: mark.attrs.damAdditional,
227
+ children: [],
228
+ });
215
229
  }
216
230
  throw new Error(`Unsupported mark "${mark.type.name}" was applied to node.`);
217
231
  };
@@ -8,6 +8,7 @@ const getNodeType = (node) => {
8
8
  'link-to-matrix-asset': Extensions_1.NodeName.Text,
9
9
  'matrix-image': Extensions_1.NodeName.AssetImage,
10
10
  'dam-image': Extensions_1.NodeName.DAMImage,
11
+ 'link-to-dam-asset': Extensions_1.NodeName.Text,
11
12
  text: 'text',
12
13
  };
13
14
  const tagMap = {
@@ -125,6 +126,18 @@ const getNodeMarks = (node) => {
125
126
  },
126
127
  });
127
128
  }
129
+ else if (node.type === 'link-to-dam-asset') {
130
+ marks.push({
131
+ type: Extensions_1.MarkName.DAMLink,
132
+ attrs: {
133
+ damSystemType: node.damSystemType,
134
+ damSystemIdentifier: node.damSystemIdentifier,
135
+ damObjectId: node.damObjectId,
136
+ damAdditional: node.damAdditional ? JSON.stringify(node.damAdditional) : undefined,
137
+ target: node.target,
138
+ },
139
+ });
140
+ }
128
141
  else if (node.type === 'tag' && node.tag === 'strong') {
129
142
  marks.push({ type: 'bold' });
130
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "2.6.4",
3
+ "version": "2.6.6",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -25,7 +25,7 @@
25
25
  "@mui/icons-material": "5.15.18",
26
26
  "@remirror/extension-react-tables": "^2.2.21",
27
27
  "@remirror/react": "2.0.35",
28
- "@squiz/dx-json-schema-lib": "^1.80.1",
28
+ "@squiz/dx-json-schema-lib": "^1.81.4",
29
29
  "@squiz/dxp-content-tools-modal": "^0.4.0",
30
30
  "clsx": "2.1.1",
31
31
  "react-hook-form": "7.51.4",
@@ -33,16 +33,16 @@
33
33
  "remirror": "2.0.39"
34
34
  },
35
35
  "devDependencies": {
36
- "@squiz/dam-resource-browser-plugin": "^3.1.0-rc.1",
36
+ "@squiz/dam-resource-browser-plugin": "^3.2.7",
37
37
  "@squiz/dxp-ai-client-react": "^1.0.0",
38
- "@squiz/matrix-resource-browser-plugin": "^3.1.0-rc.5",
39
- "@squiz/resource-browser": "^3.1.0-rc.0",
38
+ "@squiz/matrix-resource-browser-plugin": "^3.3.1",
39
+ "@squiz/resource-browser": "^3.2.2",
40
40
  "@squiz/sds": "^1.5.0",
41
41
  "@testing-library/cypress": "^10.0.2",
42
42
  "@testing-library/jest-dom": "5.16.5",
43
43
  "@testing-library/react": "14.0.0",
44
44
  "@testing-library/user-event": "14.4.3",
45
- "@types/node": "20.12.4",
45
+ "@types/node": "22.10.5",
46
46
  "@types/react": "^18.2.45",
47
47
  "@types/react-dom": "^18.2.18",
48
48
  "@vitejs/plugin-react": "3.0.0",
@@ -70,10 +70,10 @@
70
70
  "vite": "^4.5.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@squiz/dam-resource-browser-plugin": "^3.1.0-rc.1",
73
+ "@squiz/dam-resource-browser-plugin": "^3.1.0",
74
74
  "@squiz/dxp-ai-client-react": "^1.0.0",
75
- "@squiz/matrix-resource-browser-plugin": "^3.1.0-rc.5",
76
- "@squiz/resource-browser": "^3.1.0-rc.0",
75
+ "@squiz/matrix-resource-browser-plugin": "^3.1.0",
76
+ "@squiz/resource-browser": "^3.1.0",
77
77
  "@squiz/sds": "^1.5.0",
78
78
  "@types/react": "^16.14.0 || ^17 || ^18",
79
79
  "@types/react-dom": "^16.9.0 || ^17 || ^18",
@@ -442,6 +442,7 @@ describe('Formatted text editor', () => {
442
442
  onRequestChildren,
443
443
  onRequestResource,
444
444
  onSearchRequest: jest.fn(),
445
+ onRequestMatrixAsset: jest.fn(),
445
446
  } as MatrixResourceBrowserPluginProps,
446
447
  });
447
448
 
@@ -5,19 +5,20 @@ import { FromToProps } from 'remirror';
5
5
  import { Input } from '../../../../ui/Fields/Input/Input';
6
6
  import { Checkbox } from '../../../../ui/Fields/Checkbox/Checkbox';
7
7
  import { UpdateLinkProps } from '../../../../Extensions/LinkExtension/LinkExtension';
8
- import { UpdateAssetLinkProps } from '../../../../Extensions/LinkExtension/AssetLinkExtension';
9
8
  import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
10
9
  import { MarkName } from '../../../../Extensions/Extensions';
11
10
  import { DeepPartial } from '../../../../types';
12
- import { hasProperties, noEmptySpacesValidation } from '../../../../utils/validation';
11
+ import { noEmptySpacesValidation } from '../../../../utils/validation';
13
12
  import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
14
13
  import { MatrixAsset } from '../../../../ui/Fields/MatrixAsset/MatrixAsset';
14
+ import { UpdateAssetLinkProps } from '../../../../Extensions/LinkExtension/AssetLinkExtension';
15
+ import { UpdateDAMLinkProps } from '../../../../Extensions/LinkExtension/DamLinkExtension';
15
16
 
16
17
  export type LinkFormData = {
17
18
  linkType: MarkName;
18
19
  text: string;
19
20
  link: UpdateLinkProps['attrs'];
20
- assetLink: UpdateAssetLinkProps['attrs'];
21
+ assetLink: Partial<UpdateDAMLinkProps['attrs'] & UpdateAssetLinkProps['attrs']>;
21
22
  range: FromToProps;
22
23
  };
23
24
 
@@ -48,7 +49,7 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
48
49
  <form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
49
50
  <div className="squiz-fte-form-group mb-4">
50
51
  <Tabs
51
- value={linkType}
52
+ value={linkType === MarkName.DAMLink ? MarkName.AssetLink : linkType}
52
53
  options={linkTypeOptions}
53
54
  onChange={(value) => setValue('linkType', value as MarkName)}
54
55
  />
@@ -98,17 +99,35 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
98
99
  </>
99
100
  )}
100
101
  {/* Asset link form fields */}
101
- {linkType === MarkName.AssetLink && (
102
+ {(linkType === MarkName.AssetLink || linkType === MarkName.DAMLink) && (
102
103
  <>
103
104
  <div className={clsx('squiz-fte-form-group mb-2')}>
104
105
  <Controller
105
106
  control={control}
106
107
  name="assetLink"
107
108
  rules={{
108
- validate: hasProperties('An asset must be selected', ['matrixIdentifier', 'matrixAssetId']),
109
+ validate: (value) => {
110
+ if (value?.damObjectId || value?.damSystemIdentifier) {
111
+ return value.damObjectId && value.damSystemIdentifier ? true : 'A DAM asset must be selected';
112
+ }
113
+ return value?.matrixAssetId && value?.matrixIdentifier ? true : 'An asset must be selected';
114
+ },
109
115
  }}
110
116
  render={({ field: { onChange, value }, fieldState: { error } }) => (
111
- <MatrixAsset modalTitle="Insert link" value={value} onChange={onChange} error={error?.message} />
117
+ <MatrixAsset
118
+ modalTitle="Insert link"
119
+ value={value}
120
+ onChange={(newValue) => {
121
+ onChange(newValue);
122
+ const updated = newValue.target.value;
123
+ if (updated.damSystemIdentifier && updated.damObjectId) {
124
+ setValue('linkType', MarkName.DAMLink);
125
+ } else {
126
+ setValue('linkType', MarkName.AssetLink);
127
+ }
128
+ }}
129
+ error={error?.message}
130
+ />
112
131
  )}
113
132
  />
114
133
  </div>
@@ -1,8 +1,15 @@
1
1
  import '@testing-library/jest-dom';
2
2
  import { act, screen, fireEvent, waitForElementToBeRemoved, waitFor } from '@testing-library/react';
3
3
  import React from 'react';
4
- import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
4
+ import { renderWithEditor, mockResourceBrowserContext, mockResourceBrowser } from '../../../../tests';
5
5
  import LinkButton from './LinkButton';
6
+ import { ResourceBrowserResource } from '@squiz/resource-browser';
7
+
8
+ const { setSelectedResource, setShouldUseMockResourceBrowser, mockResourceBrowserImpl } = mockResourceBrowser();
9
+ jest.mock('@squiz/resource-browser', () => ({
10
+ ...jest.requireActual('@squiz/resource-browser'),
11
+ ResourceBrowser: (props: any) => mockResourceBrowserImpl(props),
12
+ }));
6
13
 
7
14
  describe('LinkButton', () => {
8
15
  const openModal = async () => {
@@ -399,4 +406,70 @@ describe('LinkButton', () => {
399
406
 
400
407
  expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(2);
401
408
  });
409
+
410
+ it('Updates an existing link to be a DAM link', async () => {
411
+ setShouldUseMockResourceBrowser(true);
412
+ setSelectedResource({
413
+ id: 'my-resource-id',
414
+ name: 'My resource',
415
+ url: 'myResourceUrl',
416
+ source: {
417
+ id: 'my-source-id',
418
+ type: 'dam',
419
+ configuration: {
420
+ externalType: 'bynder',
421
+ },
422
+ },
423
+ } as unknown as ResourceBrowserResource);
424
+
425
+ const { editor, getJsonContent } = await renderWithEditor(<LinkButton />, {
426
+ content: '<a href="https://www.example.org/my-link">Sample link</a> with text',
427
+ context: {
428
+ editor: {
429
+ matrix: {
430
+ matrixDomain: 'https://my-matrix.squiz.net',
431
+ },
432
+ },
433
+ },
434
+ });
435
+
436
+ await act(() => editor.selectText(5));
437
+
438
+ await openModal();
439
+ fireEvent.click(screen.getByRole('button', { name: 'From source' }));
440
+ await waitFor(() => {
441
+ expect(screen.getByRole('button', { name: 'Select ResourceBrowser Resource' })).toBeInTheDocument();
442
+ });
443
+ fireEvent.click(await screen.findByRole('button', { name: 'Select ResourceBrowser Resource' }));
444
+ // Close the image selection modal
445
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
446
+
447
+ expect(getJsonContent()).toEqual({
448
+ type: 'paragraph',
449
+ attrs: expect.any(Object),
450
+ content: [
451
+ {
452
+ type: 'text',
453
+ marks: [
454
+ {
455
+ type: 'DAMLink',
456
+ attrs: {
457
+ damObjectId: 'my-resource-id',
458
+ damSystemIdentifier: 'my-source-id',
459
+ damSystemType: 'bynder',
460
+ href: 'myResourceUrl',
461
+ target: '_self',
462
+ damAdditional: undefined,
463
+ },
464
+ },
465
+ ],
466
+ text: 'Sample link',
467
+ },
468
+ {
469
+ type: 'text',
470
+ text: ' with text',
471
+ },
472
+ ],
473
+ });
474
+ });
402
475
  });
@@ -5,8 +5,9 @@ import { LinkFormData } from './Form/LinkForm';
5
5
  import Button from '../../../ui/Button/Button';
6
6
  import { useActive, useCommands, useKeymap } from '@remirror/react';
7
7
  import { LinkExtension } from '../../../Extensions/LinkExtension/LinkExtension';
8
- import { CommandsExtension } from '../../../Extensions/CommandsExtension/CommandsExtension';
9
8
  import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkExtension';
9
+ import { DAMLinkExtension } from '../../../Extensions/LinkExtension/DamLinkExtension';
10
+ import { CommandsExtension } from '../../../Extensions/CommandsExtension/CommandsExtension';
10
11
  import { MarkName } from '../../../Extensions/Extensions';
11
12
  import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
12
13
  import { CodeBlockExtension } from 'remirror/dist-types/extensions';
@@ -18,8 +19,13 @@ export type LinkButtonProps = {
18
19
 
19
20
  const LinkButton = ({ inPopover = false }: LinkButtonProps) => {
20
21
  const [showModal, setShowModal] = useState(false);
21
- const { updateLink, updateAssetLink } = useCommands<AssetLinkExtension | LinkExtension | CommandsExtension>();
22
- const active = useActive<LinkExtension | AssetLinkExtension | ImageExtension | CodeBlockExtension>();
22
+ const { updateLink, updateAssetLink, updateDAMLink } = useCommands<
23
+ AssetLinkExtension | LinkExtension | CommandsExtension | DAMLinkExtension
24
+ >();
25
+ const active = useActive<
26
+ LinkExtension | AssetLinkExtension | ImageExtension | CodeBlockExtension | DAMLinkExtension
27
+ >();
28
+
23
29
  // If the image tool is active, disable the link tool as they shouldn't work at the same time
24
30
  const disabled = active.image() || active.codeBlock();
25
31
  const handleClick = () => {
@@ -34,10 +40,15 @@ const LinkButton = ({ inPopover = false }: LinkButtonProps) => {
34
40
  }, []);
35
41
 
36
42
  const handleSubmit = (data: LinkFormData) => {
37
- if (data.linkType === MarkName.AssetLink) {
38
- updateAssetLink({ text: data.text, attrs: data.assetLink, range: data.range });
39
- } else {
40
- updateLink({ text: data.text, attrs: data.link, range: data.range });
43
+ switch (data.linkType) {
44
+ case MarkName.AssetLink:
45
+ updateAssetLink({ text: data.text, attrs: data.assetLink, range: data.range });
46
+ break;
47
+ case MarkName.DAMLink:
48
+ updateDAMLink({ text: data.text, attrs: data.assetLink, range: data.range });
49
+ break;
50
+ default:
51
+ updateLink({ text: data.text, attrs: data.link, range: data.range });
41
52
  }
42
53
 
43
54
  setShowModal(false);
@@ -54,7 +65,7 @@ const LinkButton = ({ inPopover = false }: LinkButtonProps) => {
54
65
  <>
55
66
  <Button
56
67
  handleOnClick={handleClick}
57
- isActive={active.link() || active.assetLink()}
68
+ isActive={active.link() || active.assetLink() || active.DAMLink()}
58
69
  icon={<InsertLinkRoundedIcon />}
59
70
  label={`Link (${getShortcutSymbol()}+K)`}
60
71
  isDisabled={disabled}
@@ -17,13 +17,18 @@ const LinkModal = ({ onCancel, onSubmit }: LinkModalProps) => {
17
17
  helpers,
18
18
  view: { state },
19
19
  } = useRemirrorContext();
20
- const { selection, marks } = useExpandedSelection([MarkName.Link, MarkName.AssetLink]);
20
+ const { selection, marks } = useExpandedSelection([MarkName.Link, MarkName.AssetLink, MarkName.DAMLink]);
21
21
  const selectedText = helpers.getTextBetween(selection.from, selection.to, state.doc);
22
- const data = {
23
- linkType: marks[0]?.type?.name === MarkName.Link ? MarkName.Link : MarkName.AssetLink,
22
+
23
+ const data: LinkFormData = {
24
+ assetLink: { ...marks.find((m) => m.type.name === MarkName.DAMLink || m.type.name === MarkName.AssetLink)?.attrs },
25
+ link: { ...marks.find((m) => m.type.name === 'link')?.attrs },
24
26
  text: selectedText,
25
- link: { ...marks.find((mark) => mark.type.name === 'link')?.attrs },
26
- assetLink: { ...marks.find((mark) => mark.type.name === MarkName.AssetLink)?.attrs },
27
+ linkType: marks.find((m) => m.type.name === MarkName.DAMLink)
28
+ ? MarkName.DAMLink
29
+ : marks[0]?.type.name === MarkName.Link
30
+ ? MarkName.Link
31
+ : MarkName.AssetLink,
27
32
  range: { from: selection.from, to: selection.to },
28
33
  };
29
34
 
@@ -4,16 +4,17 @@ import Button from '../../../ui/Button/Button';
4
4
  import LinkOffIcon from '@mui/icons-material/LinkOff';
5
5
  import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkExtension';
6
6
  import { LinkExtension } from '../../../Extensions/LinkExtension/LinkExtension';
7
+ import { DAMLinkExtension } from '../../../Extensions/LinkExtension/DamLinkExtension';
7
8
  import { LinkButtonProps } from './LinkButton';
8
9
  import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
9
10
 
10
11
  const RemoveLinkButton = ({ inPopover = false }: LinkButtonProps) => {
11
12
  const chain = useChainedCommands();
12
- const active = useActive<LinkExtension | AssetLinkExtension>();
13
- const disabled = !active.link() && !active.assetLink();
13
+ const active = useActive<LinkExtension | AssetLinkExtension | DAMLinkExtension>();
14
+ const disabled = !active.link() && !active.assetLink() && !active.DAMLink();
14
15
 
15
16
  const handleClick = () => {
16
- chain.removeLink().removeAssetLink().focus().run();
17
+ chain.removeLink().removeAssetLink().removeDAMLink().focus().run();
17
18
  };
18
19
  const handleShortcut = useCallback(() => {
19
20
  handleClick();
@@ -30,6 +30,7 @@ import { UnsupportedNodeExtension } from './UnsuportedExtension/UnsupportedNodeE
30
30
  import { FetchUrlExtension } from './FetchUrlExtension/FetchUrlExtension';
31
31
  import { TableExtension } from '@remirror/extension-react-tables';
32
32
  import { ReactComponentExtension } from '@remirror/extension-react-component';
33
+ import { DAMLinkExtension } from './LinkExtension/DamLinkExtension';
33
34
 
34
35
  export enum NodeName {
35
36
  Image = 'image',
@@ -46,6 +47,7 @@ export enum NodeName {
46
47
  export enum MarkName {
47
48
  Link = 'link',
48
49
  AssetLink = 'assetLink',
50
+ DAMLink = 'DAMLink',
49
51
  }
50
52
 
51
53
  export const createExtensions = (context: EditorContextOptions, enableDecorations: boolean = true) => {
@@ -69,6 +71,7 @@ export const createExtensions = (context: EditorContextOptions, enableDecoration
69
71
  matrixDomain: context.matrix.matrixDomain,
70
72
  }),
71
73
  new DAMImageExtension(),
74
+ new DAMLinkExtension(),
72
75
  new LinkExtension(),
73
76
  new AssetLinkExtension({
74
77
  matrixDomain: context.matrix.matrixDomain,
@@ -51,6 +51,16 @@ export class FetchUrlExtension extends PlainExtension<FetchUrlOptions> {
51
51
  }),
52
52
  );
53
53
  }
54
+
55
+ const damLinkMark = this.findDAMLinkMark(node.marks as Mark[]);
56
+ if (node.type.name === 'text' && damLinkMark) {
57
+ promises.push(
58
+ this.fetchAndReplace(damLinkMark.attrs, (url: string) => {
59
+ const updatedMark = damLinkMark.type.create({ ...damLinkMark.attrs, href: url });
60
+ tr.addMark(pos, pos + node.nodeSize, updatedMark);
61
+ }),
62
+ );
63
+ }
54
64
  });
55
65
 
56
66
  if (promises.length) {
@@ -64,6 +74,10 @@ export class FetchUrlExtension extends PlainExtension<FetchUrlOptions> {
64
74
  return marks.find((mark) => mark.type.name === MarkName.AssetLink && mark.attrs.href === '');
65
75
  }
66
76
 
77
+ private findDAMLinkMark(marks: Mark[]): Mark | undefined {
78
+ return marks.find((mark) => mark.type.name === MarkName.DAMLink && mark.attrs.href === '');
79
+ }
80
+
67
81
  private fetchAndReplace(nodeAttrs: ResolveNodeType, onFetched: (url: string) => void): Promise<void> {
68
82
  return this.options
69
83
  .fetchUrl(nodeAttrs)
@@ -48,7 +48,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
48
48
  createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
49
49
  return {
50
50
  inclusive: false,
51
- excludes: [this.name, MarkName.Link].join(' '),
51
+ excludes: [this.name, MarkName.Link, MarkName.DAMLink].join(' '),
52
52
  ...override,
53
53
  attrs: {
54
54
  ...extra.defaults(),
@@ -0,0 +1,110 @@
1
+ import { renderWithEditor } from '../../../tests';
2
+
3
+ describe('DAMLinkExtension', () => {
4
+ it('Parses HTML content representing a DAM link', async () => {
5
+ const { getJsonContent } = await renderWithEditor(null, {
6
+ content: `<a data-dam-object-id="123"
7
+ data-dam-system-identifier="sys-id"
8
+ data-dam-system-type="type"
9
+ data-dam-additional='{"variant":"primary"}'
10
+ href="https://example.org/page"
11
+ target="_blank">
12
+ Link Text
13
+ </a>`,
14
+ });
15
+
16
+ expect(getJsonContent()).toEqual({
17
+ type: 'paragraph',
18
+ attrs: expect.any(Object),
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text: 'Link Text',
23
+ marks: [
24
+ {
25
+ type: 'DAMLink',
26
+ attrs: {
27
+ damObjectId: '123',
28
+ damSystemIdentifier: 'sys-id',
29
+ damSystemType: 'type',
30
+ damAdditional: { variant: 'primary' },
31
+ href: 'https://example.org/page',
32
+ target: '_blank',
33
+ },
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ });
39
+ });
40
+
41
+ it('Outputs expected HTML', async () => {
42
+ const { getHtmlContent } = await renderWithEditor(null, {
43
+ content: {
44
+ type: 'paragraph',
45
+ content: [
46
+ {
47
+ type: 'text',
48
+ text: 'Link Text',
49
+ marks: [
50
+ {
51
+ type: 'DAMLink',
52
+ attrs: {
53
+ damObjectId: '123',
54
+ damSystemIdentifier: 'sys-id',
55
+ damSystemType: 'type',
56
+ damAdditional: { variant: 'primary' },
57
+ href: 'https://example.org/page',
58
+ target: '_blank',
59
+ },
60
+ },
61
+ ],
62
+ },
63
+ ],
64
+ },
65
+ });
66
+
67
+ expect(getHtmlContent()).toEqual(
68
+ '<a rel="noopener noreferrer nofollow" ' +
69
+ 'href="https://example.org/page" ' +
70
+ 'target="_blank" ' +
71
+ 'data-dam-object-id="123" ' +
72
+ 'data-dam-system-identifier="sys-id" ' +
73
+ 'data-dam-system-type="type" ' +
74
+ 'data-dam-additional="{&quot;variant&quot;:&quot;primary&quot;}">' +
75
+ 'Link Text' +
76
+ '</a>',
77
+ );
78
+ });
79
+
80
+ it('Resolves to a regular link if HTML content is missing some of the expected attributes', async () => {
81
+ const { getJsonContent } = await renderWithEditor(null, {
82
+ content: `<a href="https://my-bynder-link.com"
83
+ target="_self"
84
+ data-dam-object-id="123">
85
+ Hello!
86
+ </a>`,
87
+ });
88
+
89
+ expect(getJsonContent()).toEqual({
90
+ type: 'paragraph',
91
+ attrs: expect.any(Object),
92
+ content: [
93
+ {
94
+ type: 'text',
95
+ text: 'Hello!',
96
+ marks: [
97
+ {
98
+ type: 'link',
99
+ attrs: {
100
+ href: 'https://my-bynder-link.com',
101
+ target: '_self',
102
+ title: null,
103
+ },
104
+ },
105
+ ],
106
+ },
107
+ ],
108
+ });
109
+ });
110
+ });