@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.
- package/CHANGELOG.md +8 -0
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +3 -2
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -4
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +11 -7
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +8 -4
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +2 -2
- package/lib/Extensions/Extensions.d.ts +2 -1
- package/lib/Extensions/Extensions.js +3 -0
- package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +1 -0
- package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +10 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +1 -1
- package/lib/Extensions/LinkExtension/DamLinkExtension.d.ts +29 -0
- package/lib/Extensions/LinkExtension/DamLinkExtension.js +111 -0
- package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +16 -2
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +71 -20
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +10 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +13 -0
- package/package.json +2 -2
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +26 -7
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +74 -1
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +19 -8
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +10 -5
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +4 -3
- package/src/Extensions/Extensions.ts +3 -0
- package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +14 -0
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +1 -1
- package/src/Extensions/LinkExtension/DamLinkExtension.spec.ts +110 -0
- package/src/Extensions/LinkExtension/DamLinkExtension.ts +137 -0
- package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
- package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +83 -26
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +48 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +63 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +12 -0
- 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 {
|
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:
|
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
|
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<
|
22
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
26
|
-
|
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="{"variant":"primary"}">' +
|
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(),
|