@squiz/formatted-text-editor 2.6.5 → 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 (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +3 -2
  3. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -4
  4. package/lib/EditorToolbar/Tools/Link/LinkButton.js +11 -7
  5. package/lib/EditorToolbar/Tools/Link/LinkModal.js +8 -4
  6. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +2 -2
  7. package/lib/Extensions/Extensions.d.ts +2 -1
  8. package/lib/Extensions/Extensions.js +3 -0
  9. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +1 -0
  10. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +10 -0
  11. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +1 -1
  12. package/lib/Extensions/LinkExtension/DamLinkExtension.d.ts +29 -0
  13. package/lib/Extensions/LinkExtension/DamLinkExtension.js +111 -0
  14. package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
  15. package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +16 -2
  16. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +71 -20
  17. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +10 -0
  18. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +13 -0
  19. package/package.json +2 -2
  20. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +26 -7
  21. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +74 -1
  22. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +19 -8
  23. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +10 -5
  24. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +4 -3
  25. package/src/Extensions/Extensions.ts +3 -0
  26. package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +14 -0
  27. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +1 -1
  28. package/src/Extensions/LinkExtension/DamLinkExtension.spec.ts +110 -0
  29. package/src/Extensions/LinkExtension/DamLinkExtension.ts +137 -0
  30. package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
  31. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +83 -26
  32. package/src/utils/converters/mocks/squizNodeJson.mock.ts +48 -0
  33. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +63 -0
  34. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -0
  35. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +12 -0
  36. package/src/utils/getMarkNamesByGroup.spec.ts +1 -1
@@ -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
+ });
@@ -0,0 +1,137 @@
1
+ import {
2
+ ApplySchemaAttributes,
3
+ ExtensionPriority,
4
+ FromToProps,
5
+ isElementDomNode,
6
+ MarkExtensionSpec,
7
+ MarkSpecOverride,
8
+ } from 'remirror';
9
+ import { command, CommandFunction, extension, MarkExtension, omitExtraAttributes, removeMark } from '@remirror/core';
10
+ import { LinkTarget, validateTarget } from './common';
11
+ import { CommandsExtension } from '../CommandsExtension/CommandsExtension';
12
+ import { MarkName } from '../Extensions';
13
+
14
+ export type DAMLinkAttributes = {
15
+ damObjectId: string;
16
+ damSystemIdentifier: string;
17
+ damSystemType: string;
18
+ damAdditional?: {
19
+ variant: string;
20
+ };
21
+ target: LinkTarget;
22
+ href: string;
23
+ url: string;
24
+ };
25
+
26
+ export type DAMLinkOptions = {
27
+ defaultTarget?: LinkTarget;
28
+ supportedTargets?: LinkTarget[];
29
+ };
30
+
31
+ export type UpdateDAMLinkProps = {
32
+ text: string;
33
+ attrs: Partial<DAMLinkAttributes>;
34
+ range: FromToProps;
35
+ };
36
+
37
+ @extension<DAMLinkOptions>({
38
+ defaultOptions: {
39
+ defaultTarget: LinkTarget.Self,
40
+ supportedTargets: [LinkTarget.Self, LinkTarget.Blank],
41
+ },
42
+ defaultPriority: ExtensionPriority.High,
43
+ })
44
+ export class DAMLinkExtension extends MarkExtension<DAMLinkOptions> {
45
+ get name(): string {
46
+ return MarkName.DAMLink;
47
+ }
48
+
49
+ createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
50
+ return {
51
+ inclusive: false,
52
+ excludes: [this.name, MarkName.Link, MarkName.AssetLink].join(' '),
53
+ ...override,
54
+ attrs: {
55
+ ...extra.defaults(),
56
+ damObjectId: {},
57
+ damSystemIdentifier: {},
58
+ damSystemType: {},
59
+ damAdditional: { default: undefined },
60
+ href: { default: '' },
61
+ target: { default: this.options.defaultTarget },
62
+ },
63
+ parseDOM: [
64
+ {
65
+ tag: 'a[data-dam-object-id]',
66
+ getAttrs: (node) => {
67
+ if (!isElementDomNode(node)) {
68
+ return false;
69
+ }
70
+ const damObjectId = node.getAttribute('data-dam-object-id');
71
+ const damSystemIdentifier = node.getAttribute('data-dam-system-identifier');
72
+ const damSystemType = node.getAttribute('data-dam-system-type');
73
+ let damAdditional = node.getAttribute('data-dam-additional') || undefined;
74
+
75
+ if (!damObjectId || !damSystemIdentifier || !damSystemType) {
76
+ return false;
77
+ }
78
+
79
+ if (damAdditional) {
80
+ damAdditional = JSON.parse(damAdditional);
81
+ }
82
+
83
+ return {
84
+ ...extra.parse(node),
85
+ damObjectId,
86
+ damSystemIdentifier,
87
+ damSystemType,
88
+ damAdditional,
89
+ target: validateTarget(
90
+ node.getAttribute('target'),
91
+ this.options.supportedTargets,
92
+ this.options.defaultTarget,
93
+ ),
94
+ href: node.getAttribute('href') || '',
95
+ };
96
+ },
97
+ },
98
+ ],
99
+ toDOM: (node) => {
100
+ const { damObjectId, damSystemIdentifier, damSystemType, damAdditional, target, href, ...rest } =
101
+ omitExtraAttributes(node.attrs, extra);
102
+ const rel = 'noopener noreferrer nofollow';
103
+ return [
104
+ 'a',
105
+ {
106
+ ...extra.dom(node),
107
+ ...rest,
108
+ rel,
109
+ href,
110
+ target: validateTarget(target, this.options.supportedTargets, this.options.defaultTarget),
111
+ 'data-dam-object-id': damObjectId,
112
+ 'data-dam-system-identifier': damSystemIdentifier,
113
+ 'data-dam-system-type': damSystemType,
114
+ 'data-dam-additional': damAdditional ? JSON.stringify(damAdditional) : undefined,
115
+ },
116
+ 0,
117
+ ];
118
+ },
119
+ };
120
+ }
121
+
122
+ @command()
123
+ updateDAMLink({ text, attrs, range }: UpdateDAMLinkProps): CommandFunction {
124
+ return this.store.getExtension(CommandsExtension).updateMark({
125
+ attrs: { ...attrs, href: attrs.url },
126
+ text,
127
+ range,
128
+ mark: this.type,
129
+ removeMark: !attrs.damObjectId,
130
+ });
131
+ }
132
+
133
+ @command()
134
+ removeDAMLink(): CommandFunction {
135
+ return removeMark({ type: this.type });
136
+ }
137
+ }
@@ -43,7 +43,7 @@ export class LinkExtension extends MarkExtension<LinkOptions> {
43
43
  createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
44
44
  return {
45
45
  inclusive: false,
46
- excludes: [this.name, MarkName.AssetLink].join(' '),
46
+ excludes: [this.name, MarkName.AssetLink, MarkName.DAMLink].join(' '),
47
47
  ...override,
48
48
  attrs: {
49
49
  ...extra.defaults(),