@squiz/formatted-text-editor 1.21.1-alpha.7 → 1.22.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 (121) hide show
  1. package/demo/App.tsx +52 -10
  2. package/demo/index.scss +11 -10
  3. package/jest.config.ts +0 -2
  4. package/lib/Editor/Editor.js +45 -7
  5. package/lib/Editor/EditorContext.d.ts +15 -0
  6. package/lib/Editor/EditorContext.js +15 -0
  7. package/lib/EditorToolbar/FloatingToolbar.js +11 -5
  8. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
  9. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +91 -23
  10. package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +4 -1
  11. package/lib/EditorToolbar/Tools/Image/ImageButton.js +22 -14
  12. package/lib/EditorToolbar/Tools/Image/ImageModal.js +9 -5
  13. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
  14. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +66 -14
  15. package/lib/EditorToolbar/Tools/Link/LinkButton.js +21 -13
  16. package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
  17. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -8
  18. package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
  19. package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
  20. package/lib/Extensions/Extensions.d.ts +12 -5
  21. package/lib/Extensions/Extensions.js +42 -20
  22. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
  23. package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
  24. package/lib/Extensions/ImageExtension/ImageExtension.d.ts +4 -0
  25. package/lib/Extensions/ImageExtension/ImageExtension.js +11 -0
  26. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
  27. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
  28. package/lib/Extensions/LinkExtension/LinkExtension.d.ts +19 -12
  29. package/lib/Extensions/LinkExtension/LinkExtension.js +56 -66
  30. package/lib/Extensions/LinkExtension/common.d.ts +7 -0
  31. package/lib/Extensions/LinkExtension/common.js +14 -0
  32. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
  33. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +6 -2
  34. package/lib/hooks/index.d.ts +1 -0
  35. package/lib/hooks/index.js +1 -0
  36. package/lib/hooks/useExpandedSelection.d.ts +23 -0
  37. package/lib/hooks/useExpandedSelection.js +37 -0
  38. package/lib/index.css +58 -23
  39. package/lib/index.d.ts +5 -2
  40. package/lib/index.js +9 -3
  41. package/lib/types.d.ts +3 -0
  42. package/lib/types.js +2 -0
  43. package/lib/ui/Button/Button.d.ts +2 -1
  44. package/lib/ui/Button/Button.js +4 -5
  45. package/lib/ui/Fields/Input/Input.d.ts +1 -0
  46. package/lib/ui/Fields/Input/Input.js +9 -3
  47. package/lib/ui/Modal/Modal.js +5 -3
  48. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
  49. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +174 -0
  50. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
  51. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +138 -0
  52. package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
  53. package/lib/utils/resolveMatrixAssetUrl.js +10 -0
  54. package/lib/utils/undefinedIfEmpty.d.ts +1 -0
  55. package/lib/utils/undefinedIfEmpty.js +7 -0
  56. package/package.json +10 -4
  57. package/src/Editor/Editor.spec.tsx +78 -18
  58. package/src/Editor/Editor.tsx +28 -9
  59. package/src/Editor/EditorContext.spec.tsx +26 -0
  60. package/src/Editor/EditorContext.ts +26 -0
  61. package/src/Editor/_editor.scss +20 -4
  62. package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
  63. package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
  64. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
  65. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
  66. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +250 -2
  67. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
  68. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +59 -20
  69. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +12 -10
  70. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
  71. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
  72. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +137 -26
  73. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
  74. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
  75. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
  76. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
  77. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
  78. package/src/EditorToolbar/_floating-toolbar.scss +4 -5
  79. package/src/EditorToolbar/_toolbar.scss +1 -1
  80. package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
  81. package/src/Extensions/Extensions.ts +42 -18
  82. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
  83. package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
  84. package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
  85. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
  86. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
  87. package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
  88. package/src/Extensions/LinkExtension/LinkExtension.ts +71 -85
  89. package/src/Extensions/LinkExtension/common.ts +10 -0
  90. package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +41 -0
  91. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +6 -2
  92. package/src/hooks/index.ts +1 -0
  93. package/src/hooks/useExpandedSelection.ts +44 -0
  94. package/src/index.ts +5 -2
  95. package/src/types.ts +5 -0
  96. package/src/ui/Button/Button.tsx +10 -6
  97. package/src/ui/Button/_button.scss +1 -1
  98. package/src/ui/Fields/Input/Input.spec.tsx +7 -1
  99. package/src/ui/Fields/Input/Input.tsx +23 -4
  100. package/src/ui/Modal/Modal.spec.tsx +15 -0
  101. package/src/ui/Modal/Modal.tsx +8 -4
  102. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
  103. package/src/ui/_forms.scss +14 -0
  104. package/src/utils/converters/mocks/squizNodeJson.mock.ts +271 -0
  105. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
  106. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +212 -0
  107. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +341 -0
  108. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +159 -0
  109. package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
  110. package/src/utils/resolveMatrixAssetUrl.ts +7 -0
  111. package/src/utils/undefinedIfEmpty.spec.ts +12 -0
  112. package/src/utils/undefinedIfEmpty.ts +3 -0
  113. package/tailwind.config.cjs +3 -0
  114. package/tests/renderWithEditor.tsx +28 -15
  115. package/tsconfig.json +1 -1
  116. package/lib/FormattedTextEditor.d.ts +0 -2
  117. package/lib/FormattedTextEditor.js +0 -7
  118. package/src/Editor/Editor.mock.tsx +0 -43
  119. package/src/FormattedTextEditor.spec.tsx +0 -10
  120. package/src/FormattedTextEditor.tsx +0 -3
  121. /package/tests/{select.tsx → select.ts} +0 -0
@@ -0,0 +1,54 @@
1
+ import { PlainExtension } from '@remirror/core';
2
+ import { command, CommandFunction, FromToProps, getTextSelection, MarkType } from 'remirror';
3
+ import { Attrs } from 'prosemirror-model';
4
+
5
+ export class CommandsExtension extends PlainExtension {
6
+ get name(): string {
7
+ return 'squizCommands' as const;
8
+ }
9
+
10
+ /**
11
+ * Updates the attributes of a specific mark for the current selection.
12
+ * Optionally, if text is provided it will replace the current selection.
13
+ * The cursor will be place at the end of the selection after changes.
14
+ *
15
+ * @param {object} options
16
+ */
17
+ @command()
18
+ updateMark(options: {
19
+ mark: MarkType;
20
+ attrs?: Attrs;
21
+ text?: string;
22
+ removeMark?: boolean;
23
+ range: FromToProps;
24
+ }): CommandFunction {
25
+ return (props) => {
26
+ const { tr, dispatch, view } = props;
27
+ const { mark, attrs, text, removeMark, range } = options;
28
+
29
+ const selectedText = tr.doc.textBetween(range.from, range.to);
30
+
31
+ if (text !== undefined && text !== selectedText) {
32
+ // update the text in the editor if it was updated, update the range to cover the length of the new text.
33
+ tr.insertText(text, range.from, range.to);
34
+ range.to = range.from + text.length;
35
+ }
36
+
37
+ // apply the link, or remove it if no URL was provided.
38
+ if (removeMark) {
39
+ tr.removeMark(range.from, range.to, mark);
40
+ } else {
41
+ tr.addMark(range.from, range.to, mark.create(attrs));
42
+ }
43
+
44
+ // move the cursor to the end of the link and re-focus the editor.
45
+ tr.setSelection(getTextSelection({ from: range.to, to: range.to }, tr.doc));
46
+
47
+ // apply the transaction.
48
+ dispatch?.(tr);
49
+ view?.focus();
50
+
51
+ return true;
52
+ };
53
+ }
54
+ }
@@ -7,25 +7,49 @@ import {
7
7
  UnderlineExtension,
8
8
  HistoryExtension,
9
9
  } from 'remirror/extensions';
10
+ import { Extension } from '@remirror/core';
10
11
  import { PreformattedExtension } from './PreformattedExtension/PreformattedExtension';
12
+ import { AssetLinkExtension } from './LinkExtension/AssetLinkExtension';
11
13
  import { LinkExtension } from './LinkExtension/LinkExtension';
12
14
  import { ImageExtension } from './ImageExtension/ImageExtension';
15
+ import { CommandsExtension } from './CommandsExtension/CommandsExtension';
16
+ import { EditorContextOptions } from '../Editor/EditorContext';
17
+ import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
13
18
 
14
- export const Extensions = () => [
15
- new BoldExtension(),
16
- new HeadingExtension(),
17
- new ItalicExtension(),
18
- new NodeFormattingExtension(),
19
- new ParagraphExtension(),
20
- new PreformattedExtension(),
21
- new UnderlineExtension(),
22
- new HistoryExtension(),
23
- new ImageExtension(),
24
- new LinkExtension({
25
- supportedTargets: [
26
- // '_self' is the browser default and will be used when encountering a link with a
27
- // different target is encountered.
28
- '_blank',
29
- ],
30
- }),
31
- ];
19
+ export enum NodeName {
20
+ Image = 'image',
21
+ AssetImage = 'assetImage',
22
+ Text = 'text',
23
+ }
24
+
25
+ export enum MarkName {
26
+ Link = 'link',
27
+ AssetLink = 'assetLink',
28
+ }
29
+
30
+ export const createExtensions = (context: EditorContextOptions) => {
31
+ return (): Extension[] => {
32
+ return [
33
+ new CommandsExtension(),
34
+ new BoldExtension(),
35
+ new HeadingExtension(),
36
+ new ItalicExtension(),
37
+ new NodeFormattingExtension({ indents: [] }),
38
+ new ParagraphExtension(),
39
+ new PreformattedExtension(),
40
+ new UnderlineExtension(),
41
+ new HistoryExtension(),
42
+ new ImageExtension(),
43
+ new ImageExtension({ preferPastedTextContent: false }),
44
+ new AssetImageExtension({
45
+ matrixIdentifier: context.matrix.matrixIdentifier,
46
+ matrixDomain: context.matrix.matrixDomain,
47
+ }),
48
+ new LinkExtension(),
49
+ new AssetLinkExtension({
50
+ matrixIdentifier: context.matrix.matrixIdentifier,
51
+ matrixDomain: context.matrix.matrixDomain,
52
+ }),
53
+ ];
54
+ };
55
+ };
@@ -0,0 +1,76 @@
1
+ import { renderWithEditor } from '../../../tests';
2
+
3
+ describe('AssetImageExtension', () => {
4
+ it('Parses HTML content representing an asset image', async () => {
5
+ const { getJsonContent } = await renderWithEditor(null, {
6
+ content: `<img
7
+ src="https://my-matrix.squiz.net/?a=this-is-actually-ignored"
8
+ data-matrix-asset-id="123"
9
+ data-matrix-identifier="matrix-api-identifier"
10
+ data-matrix-domain="https://matrix-domain.squiz.net" />`,
11
+ });
12
+
13
+ expect(getJsonContent()).toEqual({
14
+ type: 'paragraph',
15
+ attrs: expect.any(Object),
16
+ content: [
17
+ {
18
+ type: 'assetImage',
19
+ attrs: {
20
+ matrixAssetId: '123',
21
+ matrixDomain: 'https://matrix-domain.squiz.net',
22
+ matrixIdentifier: 'matrix-api-identifier',
23
+ },
24
+ },
25
+ ],
26
+ });
27
+ });
28
+
29
+ it('Resolves to a regular image if HTML content is missing some of the expected attributes', async () => {
30
+ const { getJsonContent } = await renderWithEditor(null, {
31
+ content: '<img src="https://my-matrix.squiz.net/?a=123" data-matrix-asset-id="123" />',
32
+ });
33
+
34
+ expect(getJsonContent()).toEqual({
35
+ type: 'paragraph',
36
+ attrs: expect.any(Object),
37
+ content: [
38
+ {
39
+ type: 'image',
40
+ attrs: expect.objectContaining({
41
+ src: 'https://my-matrix.squiz.net/?a=123',
42
+ }),
43
+ },
44
+ ],
45
+ });
46
+ });
47
+
48
+ it('Outputs expected HTML', async () => {
49
+ const { getHtmlContent } = await renderWithEditor(null, {
50
+ content: {
51
+ type: 'paragraph',
52
+ content: [
53
+ {
54
+ type: 'assetImage',
55
+ attrs: {
56
+ matrixAssetId: '123',
57
+ matrixDomain: 'https://matrix-domain.squiz.net',
58
+ matrixIdentifier: 'matrix-api-identifier',
59
+ },
60
+ },
61
+ ],
62
+ },
63
+ });
64
+
65
+ expect(getHtmlContent()).toEqual(
66
+ '<img ' +
67
+ 'src="https://matrix-domain.squiz.net/_nocache?a=123" ' +
68
+ 'data-matrix-asset-id="123" ' +
69
+ 'data-matrix-identifier="matrix-api-identifier" ' +
70
+ 'data-matrix-domain="https://matrix-domain.squiz.net" ' +
71
+ 'draggable="true">' +
72
+ '<img class="ProseMirror-separator" alt="">' +
73
+ '<br class="ProseMirror-trailingBreak">',
74
+ );
75
+ });
76
+ });
@@ -0,0 +1,111 @@
1
+ import {
2
+ ApplySchemaAttributes,
3
+ command,
4
+ extension,
5
+ ExtensionPriority,
6
+ ExtensionTag,
7
+ isElementDomNode,
8
+ NodeExtension,
9
+ NodeExtensionSpec,
10
+ NodeSpecOverride,
11
+ omitExtraAttributes,
12
+ CommandFunction,
13
+ } from '@remirror/core';
14
+ import { getTextSelection } from 'remirror';
15
+ import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
16
+ import { NodeName } from '../Extensions';
17
+
18
+ export type AssetImageOptions = {
19
+ matrixIdentifier?: string;
20
+ matrixDomain?: string;
21
+ };
22
+
23
+ export type AssetImageAttributes = {
24
+ matrixAssetId: string;
25
+ matrixIdentifier: string;
26
+ matrixDomain: string;
27
+ };
28
+
29
+ @extension<AssetImageOptions>({
30
+ defaultOptions: {
31
+ matrixIdentifier: '',
32
+ matrixDomain: '',
33
+ },
34
+ defaultPriority: ExtensionPriority.High,
35
+ })
36
+ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
37
+ get name() {
38
+ return NodeName.AssetImage as const;
39
+ }
40
+
41
+ createTags() {
42
+ return [ExtensionTag.InlineNode, ExtensionTag.Media];
43
+ }
44
+
45
+ createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
46
+ return {
47
+ inline: true,
48
+ draggable: true,
49
+ selectable: true,
50
+ ...override,
51
+ attrs: {
52
+ ...extra.defaults(),
53
+ matrixAssetId: {},
54
+ matrixIdentifier: { default: this.options.matrixIdentifier },
55
+ matrixDomain: { default: this.options.matrixDomain },
56
+ },
57
+ parseDOM: [
58
+ {
59
+ tag: 'img[data-matrix-asset-id]',
60
+ getAttrs: (node) => {
61
+ if (!isElementDomNode(node)) {
62
+ return false;
63
+ }
64
+
65
+ const matrixAssetId = node.getAttribute('data-matrix-asset-id');
66
+ const matrixIdentifier = node.getAttribute('data-matrix-identifier');
67
+ const matrixDomain = node.getAttribute('data-matrix-domain');
68
+
69
+ if (!matrixAssetId || !matrixIdentifier || !matrixDomain) {
70
+ return false;
71
+ }
72
+
73
+ return {
74
+ ...extra.parse(node),
75
+ matrixAssetId,
76
+ matrixIdentifier,
77
+ matrixDomain,
78
+ };
79
+ },
80
+ },
81
+ ],
82
+ toDOM: (node) => {
83
+ const { matrixAssetId, matrixIdentifier, matrixDomain, ...rest } = omitExtraAttributes(node.attrs, extra);
84
+
85
+ return [
86
+ 'img',
87
+ {
88
+ ...extra.dom(node),
89
+ ...rest,
90
+ src: resolveMatrixAssetUrl(String(matrixAssetId), String(matrixDomain)),
91
+ 'data-matrix-asset-id': matrixAssetId,
92
+ 'data-matrix-identifier': matrixIdentifier,
93
+ 'data-matrix-domain': matrixDomain,
94
+ },
95
+ ];
96
+ },
97
+ };
98
+ }
99
+
100
+ @command()
101
+ insertAssetImage(attrs: AssetImageAttributes): CommandFunction {
102
+ return ({ tr, dispatch }) => {
103
+ const { from, to } = getTextSelection(tr.selection, tr.doc);
104
+ const node = this.type.create(attrs);
105
+
106
+ dispatch?.(tr.replaceRangeWith(from, to, node));
107
+
108
+ return true;
109
+ };
110
+ }
111
+ }
@@ -1,3 +1,19 @@
1
1
  import { ImageExtension as RemirrorImageExtension } from 'remirror/extensions';
2
+ import { PasteRule } from 'prosemirror-paste-rules';
3
+ import { ApplySchemaAttributes, NodeSpecOverride, NodeExtensionSpec } from '@remirror/core';
2
4
 
3
- export class ImageExtension extends RemirrorImageExtension {}
5
+ export class ImageExtension extends RemirrorImageExtension {
6
+ createPasteRules(): PasteRule[] {
7
+ // override super behaviour of handling file uploads.
8
+ return [];
9
+ }
10
+
11
+ createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
12
+ const spec = super.createNodeSpec(extra, override);
13
+
14
+ return {
15
+ ...spec,
16
+ selectable: true,
17
+ };
18
+ }
19
+ }
@@ -0,0 +1,104 @@
1
+ import { renderWithEditor } from '../../../tests';
2
+
3
+ describe('AssetLinkExtension', () => {
4
+ it('Parses HTML content representing an asset link', async () => {
5
+ const { getJsonContent } = await renderWithEditor(null, {
6
+ content: `<a href="https://my-matrix.squiz.net/?a=this-is-actually-ignored"
7
+ target="_self"
8
+ data-matrix-asset-id="123"
9
+ data-matrix-identifier="matrix-api-identifier"
10
+ data-matrix-domain="https://matrix-domain.squiz.net">
11
+ Hello!
12
+ </a>`,
13
+ });
14
+
15
+ expect(getJsonContent()).toEqual({
16
+ type: 'paragraph',
17
+ attrs: expect.any(Object),
18
+ content: [
19
+ {
20
+ type: 'text',
21
+ text: 'Hello!',
22
+ marks: [
23
+ {
24
+ type: 'assetLink',
25
+ attrs: {
26
+ matrixAssetId: '123',
27
+ matrixDomain: 'https://matrix-domain.squiz.net',
28
+ matrixIdentifier: 'matrix-api-identifier',
29
+ target: '_self',
30
+ },
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ });
36
+ });
37
+
38
+ it('Resolves to a regular link if HTML content is missing some of the expected attributes', async () => {
39
+ const { getJsonContent } = await renderWithEditor(null, {
40
+ content: `<a href="https://my-matrix.squiz.net/?a=123"
41
+ target="_self"
42
+ data-matrix-asset-id="123">
43
+ Hello!
44
+ </a>`,
45
+ });
46
+
47
+ expect(getJsonContent()).toEqual({
48
+ type: 'paragraph',
49
+ attrs: expect.any(Object),
50
+ content: [
51
+ {
52
+ type: 'text',
53
+ text: 'Hello!',
54
+ marks: [
55
+ {
56
+ type: 'link',
57
+ attrs: {
58
+ href: 'https://my-matrix.squiz.net/?a=123',
59
+ target: '_self',
60
+ title: null,
61
+ },
62
+ },
63
+ ],
64
+ },
65
+ ],
66
+ });
67
+ });
68
+
69
+ it('Outputs expected HTML', async () => {
70
+ const { getHtmlContent } = await renderWithEditor(null, {
71
+ content: {
72
+ type: 'paragraph',
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: 'Hello!',
77
+ marks: [
78
+ {
79
+ type: 'assetLink',
80
+ attrs: {
81
+ matrixAssetId: '123',
82
+ matrixDomain: 'https://matrix-domain.squiz.net',
83
+ matrixIdentifier: 'matrix-api-identifier',
84
+ target: '_blank',
85
+ },
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ },
91
+ });
92
+
93
+ expect(getHtmlContent()).toEqual(
94
+ '<a rel="noopener noreferrer nofollow" ' +
95
+ 'href="https://matrix-domain.squiz.net/_nocache?a=123" ' +
96
+ 'target="_blank" ' +
97
+ 'data-matrix-asset-id="123" ' +
98
+ 'data-matrix-identifier="matrix-api-identifier" ' +
99
+ 'data-matrix-domain="https://matrix-domain.squiz.net">' +
100
+ 'Hello!' +
101
+ '</a>',
102
+ );
103
+ });
104
+ });
@@ -0,0 +1,128 @@
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
+ import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
14
+
15
+ export type AssetLinkAttributes = {
16
+ matrixAssetId: string;
17
+ matrixIdentifier: string;
18
+ matrixDomain: string;
19
+ target: LinkTarget;
20
+ };
21
+
22
+ export type AssetLinkOptions = {
23
+ matrixIdentifier?: string;
24
+ matrixDomain?: string;
25
+ defaultTarget?: LinkTarget;
26
+ supportedTargets?: LinkTarget[];
27
+ };
28
+
29
+ export type UpdateAssetLinkProps = {
30
+ text: string;
31
+ attrs: Partial<AssetLinkAttributes>;
32
+ range: FromToProps;
33
+ };
34
+
35
+ @extension<AssetLinkOptions>({
36
+ defaultOptions: {
37
+ matrixIdentifier: '',
38
+ matrixDomain: '',
39
+ defaultTarget: LinkTarget.Self,
40
+ supportedTargets: [LinkTarget.Self, LinkTarget.Blank],
41
+ },
42
+ defaultPriority: ExtensionPriority.High,
43
+ })
44
+ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
45
+ get name(): string {
46
+ return MarkName.AssetLink;
47
+ }
48
+
49
+ createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
50
+ return {
51
+ inclusive: false,
52
+ excludes: MarkName.Link,
53
+ ...override,
54
+ attrs: {
55
+ ...extra.defaults(),
56
+ matrixAssetId: {},
57
+ matrixIdentifier: { default: this.options.matrixIdentifier },
58
+ matrixDomain: { default: this.options.matrixDomain },
59
+ target: { default: this.options.defaultTarget },
60
+ },
61
+ parseDOM: [
62
+ {
63
+ tag: 'a[data-matrix-asset-id]',
64
+ getAttrs: (node) => {
65
+ if (!isElementDomNode(node)) {
66
+ return false;
67
+ }
68
+
69
+ const matrixAssetId = node.getAttribute('data-matrix-asset-id');
70
+ const matrixIdentifier = node.getAttribute('data-matrix-identifier');
71
+ const matrixDomain = node.getAttribute('data-matrix-domain');
72
+
73
+ if (!matrixAssetId || !matrixIdentifier || !matrixDomain) {
74
+ return false;
75
+ }
76
+
77
+ return {
78
+ ...extra.parse(node),
79
+ matrixAssetId,
80
+ matrixIdentifier,
81
+ matrixDomain,
82
+ target: validateTarget(
83
+ node.getAttribute('target'),
84
+ this.options.supportedTargets,
85
+ this.options.defaultTarget,
86
+ ),
87
+ };
88
+ },
89
+ },
90
+ ],
91
+ toDOM: (node) => {
92
+ const { matrixAssetId, matrixIdentifier, matrixDomain, target, ...rest } = omitExtraAttributes(
93
+ node.attrs,
94
+ extra,
95
+ );
96
+ const rel = 'noopener noreferrer nofollow';
97
+ const attrs = {
98
+ ...extra.dom(node),
99
+ ...rest,
100
+ rel,
101
+ href: resolveMatrixAssetUrl(String(matrixAssetId), String(matrixDomain)),
102
+ target: validateTarget(target, this.options.supportedTargets, this.options.defaultTarget),
103
+ 'data-matrix-asset-id': matrixAssetId,
104
+ 'data-matrix-identifier': matrixIdentifier,
105
+ 'data-matrix-domain': matrixDomain,
106
+ };
107
+
108
+ return ['a', attrs, 0];
109
+ },
110
+ };
111
+ }
112
+
113
+ @command()
114
+ updateAssetLink({ text, attrs, range }: UpdateAssetLinkProps): CommandFunction {
115
+ return this.store.getExtension(CommandsExtension).updateMark({
116
+ attrs,
117
+ text,
118
+ range,
119
+ mark: this.type,
120
+ removeMark: !attrs.matrixAssetId,
121
+ });
122
+ }
123
+
124
+ @command()
125
+ removeAssetLink(): CommandFunction {
126
+ return removeMark({ type: this.type });
127
+ }
128
+ }
@@ -0,0 +1,68 @@
1
+ import { renderWithEditor } from '../../../tests';
2
+
3
+ describe('AssetLinkExtension', () => {
4
+ it('Parses HTML content representing an asset link', async () => {
5
+ const { getJsonContent } = await renderWithEditor(null, {
6
+ content: `<a href="https://example.org/some-page"
7
+ title="Link title"
8
+ target="_blank"
9
+ rel="this is ignored">
10
+ Hello!
11
+ </a>`,
12
+ });
13
+
14
+ expect(getJsonContent()).toEqual({
15
+ type: 'paragraph',
16
+ attrs: expect.any(Object),
17
+ content: [
18
+ {
19
+ type: 'text',
20
+ text: 'Hello!',
21
+ marks: [
22
+ {
23
+ type: 'link',
24
+ attrs: {
25
+ href: 'https://example.org/some-page',
26
+ title: 'Link title',
27
+ target: '_blank',
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ ],
33
+ });
34
+ });
35
+
36
+ it('Outputs expected HTML', async () => {
37
+ const { getHtmlContent } = await renderWithEditor(null, {
38
+ content: {
39
+ type: 'paragraph',
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: 'Hello!',
44
+ marks: [
45
+ {
46
+ type: 'link',
47
+ attrs: {
48
+ href: 'https://example.org/some-page',
49
+ title: 'Link title',
50
+ target: '_blank',
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ },
57
+ });
58
+
59
+ expect(getHtmlContent()).toEqual(
60
+ '<a href="https://example.org/some-page" ' +
61
+ 'title="Link title" ' +
62
+ 'rel="noopener noreferrer nofollow" ' +
63
+ 'target="_blank">' +
64
+ 'Hello!' +
65
+ '</a>',
66
+ );
67
+ });
68
+ });