@squiz/formatted-text-editor 1.21.1-alpha.4 → 1.21.1-alpha.41

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 (118) 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 +8 -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/hooks/index.d.ts +1 -0
  34. package/lib/hooks/index.js +1 -0
  35. package/lib/hooks/useExpandedSelection.d.ts +23 -0
  36. package/lib/hooks/useExpandedSelection.js +37 -0
  37. package/lib/index.css +58 -23
  38. package/lib/index.d.ts +5 -2
  39. package/lib/index.js +9 -3
  40. package/lib/types.d.ts +3 -0
  41. package/lib/types.js +2 -0
  42. package/lib/ui/Button/Button.d.ts +2 -1
  43. package/lib/ui/Button/Button.js +4 -5
  44. package/lib/ui/Fields/Input/Input.d.ts +1 -0
  45. package/lib/ui/Fields/Input/Input.js +9 -3
  46. package/lib/ui/Modal/Modal.js +5 -3
  47. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
  48. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +174 -0
  49. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
  50. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +138 -0
  51. package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
  52. package/lib/utils/resolveMatrixAssetUrl.js +10 -0
  53. package/lib/utils/undefinedIfEmpty.d.ts +1 -0
  54. package/lib/utils/undefinedIfEmpty.js +7 -0
  55. package/package.json +10 -4
  56. package/src/Editor/Editor.spec.tsx +78 -18
  57. package/src/Editor/Editor.tsx +28 -9
  58. package/src/Editor/EditorContext.spec.tsx +26 -0
  59. package/src/Editor/EditorContext.ts +26 -0
  60. package/src/Editor/_editor.scss +20 -4
  61. package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
  62. package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
  63. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
  64. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
  65. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +216 -1
  66. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
  67. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +47 -8
  68. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +11 -10
  69. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
  70. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
  71. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +116 -26
  72. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
  73. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
  74. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
  75. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
  76. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
  77. package/src/EditorToolbar/_floating-toolbar.scss +4 -5
  78. package/src/EditorToolbar/_toolbar.scss +1 -1
  79. package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
  80. package/src/Extensions/Extensions.ts +42 -18
  81. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
  82. package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
  83. package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
  84. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
  85. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
  86. package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
  87. package/src/Extensions/LinkExtension/LinkExtension.ts +71 -85
  88. package/src/Extensions/LinkExtension/common.ts +10 -0
  89. package/src/hooks/index.ts +1 -0
  90. package/src/hooks/useExpandedSelection.ts +44 -0
  91. package/src/index.ts +5 -2
  92. package/src/types.ts +5 -0
  93. package/src/ui/Button/Button.tsx +10 -6
  94. package/src/ui/Button/_button.scss +1 -1
  95. package/src/ui/Fields/Input/Input.spec.tsx +7 -1
  96. package/src/ui/Fields/Input/Input.tsx +23 -4
  97. package/src/ui/Modal/Modal.spec.tsx +15 -0
  98. package/src/ui/Modal/Modal.tsx +8 -4
  99. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
  100. package/src/ui/_forms.scss +14 -0
  101. package/src/utils/converters/mocks/squizNodeJson.mock.ts +271 -0
  102. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
  103. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +212 -0
  104. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +341 -0
  105. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +159 -0
  106. package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
  107. package/src/utils/resolveMatrixAssetUrl.ts +7 -0
  108. package/src/utils/undefinedIfEmpty.spec.ts +12 -0
  109. package/src/utils/undefinedIfEmpty.ts +3 -0
  110. package/tailwind.config.cjs +3 -0
  111. package/tests/renderWithEditor.tsx +28 -15
  112. package/tsconfig.json +1 -1
  113. package/lib/FormattedTextEditor.d.ts +0 -2
  114. package/lib/FormattedTextEditor.js +0 -7
  115. package/src/Editor/Editor.mock.tsx +0 -43
  116. package/src/FormattedTextEditor.spec.tsx +0 -10
  117. package/src/FormattedTextEditor.tsx +0 -3
  118. /package/tests/{select.tsx → select.ts} +0 -0
@@ -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
+ });
@@ -1,26 +1,55 @@
1
- import { ApplySchemaAttributes, getMarkRanges, isElementDomNode, MarkExtensionSpec, MarkSpecOverride } from 'remirror';
2
- import { getTextSelection, getMarkRange, KeyBindingProps } from '@remirror/core';
3
- import { CommandFunction } from '@remirror/pm';
4
- import { LinkAttributes as RemirrorLinkAttributes, LinkExtension as RemirrorLinkExtension } from 'remirror/extensions';
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 LinkAttributes = {
15
+ href: string;
16
+ title?: string;
17
+ target: LinkTarget;
18
+ };
5
19
 
6
- export type UpdateLinkOptions = LinkAttributes & {
7
- text: string;
20
+ export type LinkOptions = {
21
+ defaultTarget?: LinkTarget;
22
+ supportedTargets?: LinkTarget[];
8
23
  };
9
24
 
10
- export type LinkAttributes = RemirrorLinkAttributes & {
11
- title?: string;
25
+ export type UpdateLinkProps = {
26
+ text: string;
27
+ attrs: Partial<LinkAttributes>;
28
+ range: FromToProps;
12
29
  };
13
30
 
14
- export class LinkExtension extends RemirrorLinkExtension {
15
- createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
16
- const spec = super.createMarkSpec(extra, override);
31
+ @extension<LinkOptions>({
32
+ defaultOptions: {
33
+ defaultTarget: LinkTarget.Self,
34
+ supportedTargets: [LinkTarget.Self, LinkTarget.Blank],
35
+ },
36
+ defaultPriority: ExtensionPriority.Medium,
37
+ })
38
+ export class LinkExtension extends MarkExtension<LinkOptions> {
39
+ get name(): string {
40
+ return MarkName.Link;
41
+ }
17
42
 
43
+ createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
18
44
  return {
19
- ...spec,
20
- excludes: undefined,
45
+ inclusive: false,
46
+ excludes: MarkName.AssetLink,
47
+ ...override,
21
48
  attrs: {
22
- ...spec.attrs,
49
+ ...extra.defaults(),
50
+ href: {},
23
51
  title: { default: undefined },
52
+ target: { default: this.options.defaultTarget },
24
53
  },
25
54
  parseDOM: [
26
55
  {
@@ -32,88 +61,45 @@ export class LinkExtension extends RemirrorLinkExtension {
32
61
 
33
62
  return {
34
63
  ...extra.parse(node),
35
- auto: false,
36
64
  href: node.getAttribute('href'),
37
- target: node.getAttribute('target'),
38
65
  title: node.getAttribute('title'),
66
+ target: validateTarget(
67
+ node.getAttribute('target'),
68
+ this.options.supportedTargets,
69
+ this.options.defaultTarget,
70
+ ),
39
71
  };
40
72
  },
41
73
  },
42
74
  ],
75
+ toDOM: (node) => {
76
+ const { target, ...rest } = omitExtraAttributes(node.attrs, extra);
77
+ const rel = 'noopener noreferrer nofollow';
78
+ const attrs = {
79
+ ...extra.dom(node),
80
+ ...rest,
81
+ rel,
82
+ target: validateTarget(target, this.options.supportedTargets, this.options.defaultTarget),
83
+ };
84
+
85
+ return ['a', attrs, 0];
86
+ },
43
87
  };
44
88
  }
45
89
 
46
- shortcut({ tr }: KeyBindingProps): boolean {
47
- // override parent implementation to allow a link to be inserted without requiring a text selection first.
48
- const { from, to, $from } = tr.selection;
49
- const mark = getMarkRange($from, this.type);
50
- const selectedText = tr.doc.textBetween(from, to);
51
-
52
- this.options.onShortcut({
53
- activeLink: mark ? { attrs: mark.mark.attrs as LinkAttributes, from: mark.from, to: mark.to } : undefined,
54
- selectedText,
55
- from,
56
- to,
90
+ @command()
91
+ updateLink({ text, attrs, range }: UpdateLinkProps): CommandFunction {
92
+ return this.store.getExtension(CommandsExtension).updateMark({
93
+ attrs,
94
+ text,
95
+ range,
96
+ mark: this.type,
97
+ removeMark: !attrs.href,
57
98
  });
58
-
59
- return true;
60
99
  }
61
100
 
62
- selectLink(): CommandFunction {
63
- // parent implementation selects only the link text, this mimics closer to Google Docs where
64
- // the select text is widened to include the link but retains non-link selected text as well.
65
- return (props) => {
66
- const { tr } = props;
67
- const ranges = getMarkRanges(tr.selection, this.type);
68
-
69
- if (ranges.length === 0) {
70
- return false;
71
- }
72
-
73
- // work out the start position of the first link and end position of the last link in the selection.
74
- const from = Math.min(tr.selection.from, ...ranges.map((range) => range.from));
75
- const to = Math.max(tr.selection.to, ...ranges.map((range) => range.to));
76
-
77
- // don't need to widen the selection, return early.
78
- if (tr.selection.from === from && tr.selection.to === to) {
79
- return false;
80
- }
81
-
82
- // widen the selection to make sure the full link is included.
83
- this.store.commands.selectText.original({ from, to })(props);
84
- return true;
85
- };
86
- }
87
-
88
- updateLink(options: UpdateLinkOptions): CommandFunction {
89
- const { text, ...attrs } = options;
90
-
91
- return (props) => {
92
- const { tr, dispatch, view } = props;
93
- const range = { from: tr.selection.from, to: tr.selection.to };
94
- const selectedText = tr.doc.textBetween(range.from, range.to);
95
-
96
- if (text !== selectedText) {
97
- // update the text in the editor if it was updated, update the range to cover the length of the new text.
98
- tr.insertText(text);
99
- range.to = range.from + text.length;
100
- }
101
-
102
- // apply the link, or remove it if no URL was provided.
103
- if (attrs.href.length > 0) {
104
- tr.addMark(range.from, range.to, this.type.create(attrs));
105
- } else {
106
- tr.removeMark(range.from, range.to, this.type);
107
- }
108
-
109
- // move the cursor to the end of the link and re-focus the editor.
110
- tr.setSelection(getTextSelection({ from: range.to, to: range.to }, tr.doc));
111
-
112
- // apply the transaction.
113
- dispatch?.(tr);
114
- view?.focus();
115
-
116
- return true;
117
- };
101
+ @command()
102
+ removeLink(): CommandFunction {
103
+ return removeMark({ type: this.type });
118
104
  }
119
105
  }
@@ -0,0 +1,10 @@
1
+ export enum LinkTarget {
2
+ Self = '_self',
3
+ Blank = '_blank',
4
+ Parent = '_parent',
5
+ Top = '_top',
6
+ }
7
+
8
+ export const validateTarget = (target: unknown, supportedTargets: LinkTarget[], defaultTarget: LinkTarget) => {
9
+ return supportedTargets.includes(target as LinkTarget) ? (target as LinkTarget) : defaultTarget;
10
+ };
@@ -1 +1,2 @@
1
1
  export * from './useExtensionNames';
2
+ export * from './useExpandedSelection';
@@ -0,0 +1,44 @@
1
+ import { Selection } from '@remirror/core';
2
+ import { GetMarkRange, getMarkRanges, getTextSelection, Mark, MarkType } from 'remirror';
3
+ import { useEditorState } from '@remirror/react';
4
+
5
+ export type ExpandedSelection = {
6
+ selection: Selection;
7
+ marks: Mark[];
8
+ };
9
+
10
+ /**
11
+ * Returns a single range that is either:
12
+ * 1. The current selection if the current selection does not contain any of the indicated marks.
13
+ * 2. An expanded version of the current selection if the range contains any of the indicated marks.
14
+ *
15
+ * For example, given the content:
16
+ *
17
+ * <strong>Bold</strong> regular <u>underline</u> <u>more underline</u>
18
+ *
19
+ * If the marks passed in are 'bold' and 'underline' and the text "old regular under" is selected
20
+ * the returned range will be "Bold regular underline".
21
+ *
22
+ * @param {(MarkType|string)[]} markTypes
23
+ *
24
+ * @return {ExpandedSelection}
25
+ */
26
+ export const useExpandedSelection = (markTypes: (MarkType | string)[]): ExpandedSelection => {
27
+ const { doc, selection } = useEditorState();
28
+ const ranges: GetMarkRange[] = [];
29
+
30
+ markTypes.forEach((markType) => {
31
+ ranges.push(...getMarkRanges(selection, markType));
32
+ });
33
+
34
+ if (ranges.length === 0) {
35
+ return { selection, marks: [] };
36
+ }
37
+
38
+ // work out the start position of the first link and end position of the last link in the selection.
39
+ const from = Math.min(selection.from, ...ranges.map((range) => range.from));
40
+ const to = Math.max(selection.to, ...ranges.map((range) => range.to));
41
+ const marks = ranges.map((range) => range.mark);
42
+
43
+ return { selection: getTextSelection({ from, to }, doc), marks };
44
+ };
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
- import FormattedTextEditor from './FormattedTextEditor';
1
+ import Editor from './Editor/Editor';
2
+ import { EditorContext } from './Editor/EditorContext';
3
+ import { remirrorNodeToSquizNode } from './utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode';
4
+ import { squizNodeToRemirrorNode } from './utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode';
2
5
 
3
- export { FormattedTextEditor };
6
+ export { Editor, EditorContext, remirrorNodeToSquizNode, squizNodeToRemirrorNode };