@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,341 @@
1
+ import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
2
+ import { RemirrorJSON } from '@remirror/core';
3
+ import { squizNodeToRemirrorNode } from './squizNodeToRemirrorNode';
4
+ import {
5
+ mockSquizNodeJson,
6
+ mockSquizNodeTextJson,
7
+ sharedNodeExamples,
8
+ squizOnlyNodeExamples,
9
+ } from '../mocks/squizNodeJson.mock';
10
+
11
+ type FormattedText = FormattedTextModels.v1.FormattedText;
12
+
13
+ describe('squizNodeToRemirrorNode', () => {
14
+ it('should convert complex Squiz component JSON to Remirror JSON', () => {
15
+ expect(squizNodeToRemirrorNode(mockSquizNodeJson)).toEqual({
16
+ type: 'doc',
17
+ content: [
18
+ {
19
+ type: 'paragraph',
20
+ attrs: {
21
+ nodeIndent: null,
22
+ nodeTextAlignment: null,
23
+ nodeLineHeight: null,
24
+ style: '',
25
+ },
26
+ content: [
27
+ {
28
+ type: 'text',
29
+ text: 'Hello ',
30
+ },
31
+ {
32
+ type: 'text',
33
+ marks: [
34
+ {
35
+ type: 'link',
36
+ attrs: {
37
+ href: 'https://www.google.com',
38
+ target: null,
39
+ auto: false,
40
+ title: null,
41
+ },
42
+ },
43
+ {
44
+ type: 'bold',
45
+ },
46
+ ],
47
+ text: 'Mr Bean',
48
+ },
49
+ {
50
+ type: 'text',
51
+ text: ', nice to ',
52
+ },
53
+ {
54
+ type: 'text',
55
+ marks: [
56
+ {
57
+ type: 'link',
58
+ attrs: {
59
+ href: 'https://www.google.com',
60
+ target: null,
61
+ auto: false,
62
+ title: null,
63
+ },
64
+ },
65
+ ],
66
+ text: 'meet you',
67
+ },
68
+ {
69
+ type: 'text',
70
+ text: '.',
71
+ },
72
+ {
73
+ type: 'image',
74
+ attrs: {
75
+ alt: 'Test',
76
+ height: '150',
77
+ width: '200',
78
+ src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
79
+ title: '',
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ ],
85
+ });
86
+ });
87
+
88
+ it('should convert top level text component JSON to Remirror JSON', () => {
89
+ expect(squizNodeToRemirrorNode(mockSquizNodeTextJson)).toEqual({
90
+ content: [
91
+ {
92
+ type: 'paragraph',
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: 'Hello world!',
97
+ },
98
+ {
99
+ type: 'text',
100
+ text: 'Another one...',
101
+ },
102
+ ],
103
+ },
104
+ ],
105
+ type: 'doc',
106
+ });
107
+ });
108
+
109
+ it('should handle empty Squiz component JSON', () => {
110
+ expect(squizNodeToRemirrorNode([])).toEqual({ content: [], type: 'doc' });
111
+ expect(
112
+ squizNodeToRemirrorNode([
113
+ {
114
+ children: [],
115
+ type: 'tag',
116
+ tag: 'p',
117
+ },
118
+ ]),
119
+ ).toEqual({
120
+ content: [
121
+ {
122
+ attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
123
+ type: 'paragraph',
124
+ },
125
+ ],
126
+ type: 'doc',
127
+ });
128
+ });
129
+
130
+ it.each([
131
+ [
132
+ 'Unsupported DOM tag',
133
+ [
134
+ {
135
+ children: [
136
+ {
137
+ type: 'text',
138
+ value: 'Should throw an error.',
139
+ },
140
+ ],
141
+ type: 'tag',
142
+ tag: 'code',
143
+ },
144
+ ],
145
+ 'Unsupported node type provided: tag (tag: code)',
146
+ ],
147
+ [
148
+ 'Unsupported node type',
149
+ [
150
+ {
151
+ children: [
152
+ {
153
+ type: 'text',
154
+ value: 'Should throw an error.',
155
+ },
156
+ ],
157
+ type: 'unsupported-type',
158
+ },
159
+ ],
160
+ 'Unsupported node type provided: unsupported-type',
161
+ ],
162
+ ])('should throw an error for non supported node types', (description: string, node: any, expectedError: string) => {
163
+ expect(() => squizNodeToRemirrorNode(node)).toThrow(expectedError);
164
+ });
165
+
166
+ it('should handle pre formatted text', () => {
167
+ const squizComponentJSON: FormattedText = [
168
+ {
169
+ children: [
170
+ {
171
+ type: 'text',
172
+ value: 'Hello world!',
173
+ },
174
+ ],
175
+ type: 'tag',
176
+ tag: 'pre',
177
+ },
178
+ ];
179
+
180
+ const expected: RemirrorJSON = {
181
+ content: [
182
+ {
183
+ attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
184
+ content: [{ text: 'Hello world!', type: 'text' }],
185
+ type: 'preformatted',
186
+ },
187
+ ],
188
+ type: 'doc',
189
+ };
190
+
191
+ const result = squizNodeToRemirrorNode(squizComponentJSON);
192
+ expect(result).toEqual(expected);
193
+ });
194
+
195
+ it('should handle images', () => {
196
+ const squizComponentJSON: FormattedText = [
197
+ {
198
+ children: [
199
+ {
200
+ children: [],
201
+ attributes: {
202
+ alt: 'This is a test alt',
203
+ height: '360',
204
+ width: '480',
205
+ src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
206
+ title: '',
207
+ },
208
+ type: 'tag',
209
+ tag: 'img',
210
+ },
211
+ ],
212
+ type: 'tag',
213
+ tag: 'p',
214
+ },
215
+ ];
216
+
217
+ const expected: RemirrorJSON = {
218
+ content: [
219
+ {
220
+ attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
221
+ content: [
222
+ {
223
+ type: 'image',
224
+ attrs: {
225
+ alt: 'This is a test alt',
226
+ height: '360',
227
+ width: '480',
228
+ src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
229
+ title: '',
230
+ },
231
+ },
232
+ ],
233
+ type: 'paragraph',
234
+ },
235
+ ],
236
+ type: 'doc',
237
+ };
238
+
239
+ const result = squizNodeToRemirrorNode(squizComponentJSON);
240
+ expect(result).toEqual(expected);
241
+ });
242
+
243
+ it.each([
244
+ ['italics', 'italic'],
245
+ ['bold', 'bold'],
246
+ ['underline', 'underline'],
247
+ ])('should handle %s formatting', (a, b) => {
248
+ const squizComponentJSON: FormattedText = [
249
+ {
250
+ children: [
251
+ {
252
+ children: [
253
+ {
254
+ type: 'text',
255
+ value: 'Hello world!',
256
+ },
257
+ ],
258
+ font: {
259
+ [a]: true,
260
+ },
261
+ tag: 'span',
262
+ type: 'tag',
263
+ },
264
+ ],
265
+ type: 'tag',
266
+ tag: 'p',
267
+ },
268
+ ];
269
+
270
+ const expected: RemirrorJSON = {
271
+ content: [
272
+ {
273
+ attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
274
+ content: [{ text: 'Hello world!', marks: [{ type: b }], type: 'text' }],
275
+ type: 'paragraph',
276
+ },
277
+ ],
278
+ type: 'doc',
279
+ };
280
+
281
+ const result = squizNodeToRemirrorNode(squizComponentJSON);
282
+ expect(result).toEqual(expected);
283
+ });
284
+
285
+ it.each([1, 2, 3, 4, 5, 6])('should handle heading %s and set it to level %s', (level) => {
286
+ const squizComponentJSON: FormattedText = [
287
+ {
288
+ children: [
289
+ {
290
+ type: 'text',
291
+ value: 'Hello world!',
292
+ },
293
+ ],
294
+ type: 'tag',
295
+ tag: `h${level}`,
296
+ },
297
+ ];
298
+
299
+ const expected: RemirrorJSON = {
300
+ content: [
301
+ {
302
+ attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '', level },
303
+ content: [{ text: 'Hello world!', type: 'text' }],
304
+ type: 'heading',
305
+ },
306
+ ],
307
+ type: 'doc',
308
+ };
309
+
310
+ const result = squizNodeToRemirrorNode(squizComponentJSON);
311
+ expect(result).toEqual(expected);
312
+ });
313
+
314
+ it.each([...sharedNodeExamples, ...squizOnlyNodeExamples])(
315
+ 'should convert a Squiz node to the expected Remirror representation - $description',
316
+ async ({ remirrorNode, squizNode }: any) => {
317
+ const result = squizNodeToRemirrorNode([
318
+ {
319
+ type: 'tag',
320
+ tag: 'p',
321
+ children: squizNode,
322
+ },
323
+ ]);
324
+ expect(result).toEqual({
325
+ type: 'doc',
326
+ content: [
327
+ {
328
+ type: 'paragraph',
329
+ attrs: {
330
+ nodeIndent: null,
331
+ nodeLineHeight: null,
332
+ nodeTextAlignment: null,
333
+ style: '',
334
+ },
335
+ content: [remirrorNode],
336
+ },
337
+ ],
338
+ });
339
+ },
340
+ );
341
+ });
@@ -0,0 +1,159 @@
1
+ import { RemirrorJSON, ObjectMark } from '@remirror/core';
2
+ import { Attrs } from 'prosemirror-model';
3
+ import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
4
+ import { undefinedIfEmpty } from '../../undefinedIfEmpty';
5
+ import { MarkName, NodeName } from '../../../Extensions/Extensions';
6
+
7
+ type FormattedText = FormattedTextModels.v1.FormattedText;
8
+ type FormattedNodes = FormattedTextModels.v1.FormattedNodes;
9
+
10
+ const getNodeType = (node: FormattedNodes): string => {
11
+ const typeMap: Record<string, string> = {
12
+ 'link-to-matrix-asset': NodeName.Text,
13
+ 'matrix-image': NodeName.AssetImage,
14
+ text: 'text',
15
+ };
16
+
17
+ const tagMap: Record<string, string> = {
18
+ h1: 'heading',
19
+ h2: 'heading',
20
+ h3: 'heading',
21
+ h4: 'heading',
22
+ h5: 'heading',
23
+ h6: 'heading',
24
+ img: 'image',
25
+ pre: 'preformatted',
26
+ p: 'paragraph',
27
+ a: NodeName.Text,
28
+ span: NodeName.Text,
29
+ };
30
+
31
+ if (typeMap[node.type]) {
32
+ return typeMap[node.type];
33
+ }
34
+
35
+ if (node.type === 'tag' && tagMap[node.tag]) {
36
+ return tagMap[node.tag];
37
+ }
38
+
39
+ // Unsupported node type
40
+ throw new Error(
41
+ node.type === 'tag'
42
+ ? `Unsupported node type provided: ${node.type} (tag: ${node.tag})`
43
+ : `Unsupported node type provided: ${node.type}`,
44
+ );
45
+ };
46
+
47
+ const getNodeAttributes = (node: FormattedNodes): Attrs => {
48
+ if (node.type === 'tag' && node.tag === 'img') {
49
+ return {
50
+ alt: node.attributes?.alt,
51
+ height: node.attributes?.height,
52
+ width: node.attributes?.width,
53
+ src: node.attributes?.src,
54
+ title: node.attributes?.title,
55
+ };
56
+ } else if (node.type === 'matrix-image') {
57
+ return {
58
+ matrixAssetId: node.matrixAssetId,
59
+ matrixDomain: node.matrixDomain,
60
+ matrixIdentifier: node.matrixIdentifier,
61
+ };
62
+ } else if (node.type === 'tag') {
63
+ return {
64
+ nodeIndent: null,
65
+ nodeTextAlignment: node.formattingOptions?.alignment || null,
66
+ nodeLineHeight: null,
67
+ style: '',
68
+ level: node.tag?.startsWith('h') ? parseInt(node.tag.substring(1)) : undefined,
69
+ };
70
+ }
71
+
72
+ return {};
73
+ };
74
+
75
+ const getNodeMarks = (node: FormattedNodes): ObjectMark[] => {
76
+ const marks: ObjectMark[] = [];
77
+
78
+ if (node.type === 'tag' && node.tag === 'a') {
79
+ marks.push({
80
+ type: MarkName.Link,
81
+ attrs: {
82
+ href: node.attributes?.href,
83
+ target: node.attributes?.target ?? null,
84
+ auto: false,
85
+ title: node.attributes?.title ?? null,
86
+ },
87
+ });
88
+ } else if (node.type === 'link-to-matrix-asset') {
89
+ marks.push({
90
+ type: MarkName.AssetLink,
91
+ attrs: {
92
+ matrixAssetId: node.matrixAssetId,
93
+ matrixDomain: node.matrixDomain,
94
+ matrixIdentifier: node.matrixIdentifier,
95
+ target: node.target,
96
+ },
97
+ });
98
+ }
99
+
100
+ // Handle font formatting
101
+ if ('font' in node) {
102
+ node.font?.bold && marks.push({ type: 'bold' });
103
+ node.font?.italics && marks.push({ type: 'italic' });
104
+ node.font?.underline && marks.push({ type: 'underline' });
105
+ }
106
+
107
+ return marks;
108
+ };
109
+
110
+ const unwrapNodeIfNeeded = (node: RemirrorJSON): RemirrorJSON[] => {
111
+ if (node.type === 'text' && node.content?.length) {
112
+ return node.content.map((child) => {
113
+ return {
114
+ ...child,
115
+ marks: [...(child.marks || []), ...(node.marks || [])],
116
+ };
117
+ });
118
+ }
119
+
120
+ return [node];
121
+ };
122
+
123
+ const formatNode = (node: FormattedNodes): RemirrorJSON[] => {
124
+ const children: RemirrorJSON[] = [];
125
+
126
+ if ('children' in node) {
127
+ node.children.forEach((child: FormattedNodes) => {
128
+ children.push(...formatNode(child));
129
+ });
130
+ }
131
+
132
+ return unwrapNodeIfNeeded({
133
+ type: getNodeType(node),
134
+ attrs: undefinedIfEmpty(getNodeAttributes(node)),
135
+ marks: undefinedIfEmpty(getNodeMarks(node)),
136
+ text: node.type === 'text' ? node.value : undefined,
137
+ content: undefinedIfEmpty(children),
138
+ });
139
+ };
140
+
141
+ /**
142
+ * Converts Squiz component JSON structure to Remirror node JSON structure.
143
+ * @param {FormattedText} nodes Squiz nodes to convert to Remirror.
144
+ * @export
145
+ * @returns {RemirrorJSON} The converted Remirror JSON.
146
+ */
147
+ export const squizNodeToRemirrorNode = (nodes: FormattedText): RemirrorJSON => {
148
+ let children: RemirrorJSON[] = [];
149
+
150
+ nodes.forEach((node) => {
151
+ children.push(...formatNode(node));
152
+ });
153
+
154
+ if (children.find((child) => child.type === 'text')) {
155
+ children = [{ type: 'paragraph', content: children }];
156
+ }
157
+
158
+ return { type: 'doc', content: children };
159
+ };
@@ -0,0 +1,26 @@
1
+ import { resolveMatrixAssetUrl } from './resolveMatrixAssetUrl';
2
+
3
+ describe('resolveMatrixAssetUrl', () => {
4
+ it.each([
5
+ [
6
+ 'domain with no scheme',
7
+ '123:hello?.txt',
8
+ 'matrix.labs.squiz.test',
9
+ 'http://matrix.labs.squiz.test/_nocache?a=123%3Ahello%3F.txt',
10
+ ],
11
+ [
12
+ 'domain with scheme',
13
+ '123:hello?.txt',
14
+ 'https://matrix.labs.squiz.test',
15
+ 'https://matrix.labs.squiz.test/_nocache?a=123%3Ahello%3F.txt',
16
+ ],
17
+ [
18
+ 'domain with path',
19
+ '123:hello?.txt',
20
+ 'https://matrix.labs.squiz.test/site-1',
21
+ 'https://matrix.labs.squiz.test/_nocache?a=123%3Ahello%3F.txt',
22
+ ],
23
+ ])('Resolves to expected URL for %s', (description: string, id: string, matrixDomain: string, expected: string) => {
24
+ expect(resolveMatrixAssetUrl(id, matrixDomain)).toBe(expected);
25
+ });
26
+ });
@@ -0,0 +1,7 @@
1
+ export const resolveMatrixAssetUrl = (id: string, matrixDomain: string): string => {
2
+ if (matrixDomain.indexOf('://') < 0) {
3
+ matrixDomain = `${window.location.protocol}//${matrixDomain}`;
4
+ }
5
+
6
+ return new URL(`/_nocache?a=${encodeURIComponent(id)}`, matrixDomain).toString();
7
+ };
@@ -0,0 +1,12 @@
1
+ import { undefinedIfEmpty } from './undefinedIfEmpty';
2
+
3
+ describe('undefinedIfEmpty', () => {
4
+ it.each([
5
+ ['empty object', {}, undefined],
6
+ ['non-empty object', { a: 'not empty' }, { a: 'not empty' }],
7
+ ['empty array', [], undefined],
8
+ ['non-empty array', ['not empty'], ['not empty']],
9
+ ])('Returns expected for %s', (description: string, object: object, expected: object | undefined) => {
10
+ expect(undefinedIfEmpty(object)).toEqual(expected);
11
+ });
12
+ });
@@ -0,0 +1,3 @@
1
+ export const undefinedIfEmpty = <T extends object>(object: T): T | undefined => {
2
+ return Object.keys(object).length > 0 ? object : undefined;
3
+ };
@@ -75,6 +75,9 @@ module.exports = {
75
75
  300: '#0774d2',
76
76
  400: '#044985',
77
77
  },
78
+ red: {
79
+ 300: '#d72321',
80
+ },
78
81
  },
79
82
  },
80
83
  },
@@ -1,19 +1,26 @@
1
- import React, { ReactElement, useEffect } from 'react';
1
+ import React, { ReactElement, useContext, useEffect } from 'react';
2
2
  import { render, RenderOptions } from '@testing-library/react';
3
3
  import { Extension, RemirrorContentType, RemirrorManager } from '@remirror/core';
4
4
  import { CorePreset } from '@remirror/preset-core';
5
5
  import { BuiltinPreset } from 'remirror';
6
6
  import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
7
- import { Extensions } from '../src/Extensions/Extensions';
8
7
  import { RemirrorTestChain } from 'jest-remirror';
8
+ import merge from 'deepmerge';
9
+ import { createExtensions } from '../src/Extensions/Extensions';
10
+ import { EditorContext } from '../src/Editor/EditorContext';
11
+ import { FloatingToolbar } from '../src/EditorToolbar/FloatingToolbar';
12
+ import { defaultEditorContext, EditorContextOptions } from '../src/Editor/EditorContext';
13
+ import { DeepPartial } from '../src/types';
9
14
 
10
15
  export type EditorRenderOptions = RenderOptions & {
11
16
  content?: RemirrorContentType;
17
+ editable?: boolean;
12
18
  extensions?: Extension[];
19
+ context?: DeepPartial<EditorContextOptions>;
13
20
  };
14
21
 
15
22
  type TestEditorProps = EditorRenderOptions & {
16
- children: ReactElement;
23
+ children: ReactElement | null;
17
24
  onReady: (manager: RemirrorManager<Extension>) => void;
18
25
  };
19
26
 
@@ -27,9 +34,10 @@ type EditorRenderResult = {
27
34
  };
28
35
  };
29
36
 
30
- const TestEditor = ({ children, extensions, content, onReady }: TestEditorProps) => {
37
+ const TestEditor = ({ children, extensions, content, onReady, editable }: TestEditorProps) => {
38
+ const context = useContext(EditorContext);
31
39
  const { manager, state, setState } = useRemirror({
32
- extensions: () => extensions || Extensions(),
40
+ extensions: () => extensions || createExtensions(context)(),
33
41
  content: content,
34
42
  selection: 'start',
35
43
  stringHandler: 'html',
@@ -43,12 +51,14 @@ const TestEditor = ({ children, extensions, content, onReady }: TestEditorProps)
43
51
  <Remirror
44
52
  manager={manager}
45
53
  state={state}
54
+ editable={editable}
46
55
  onChange={(params) => {
47
56
  setState(params.state);
48
57
  }}
49
58
  >
50
59
  {children}
51
60
  <EditorComponent />
61
+ {editable && <FloatingToolbar />}
52
62
  </Remirror>
53
63
  );
54
64
  };
@@ -76,9 +86,10 @@ const TestEditor = ({ children, extensions, content, onReady }: TestEditorProps)
76
86
  * @return {Promise<EditorRenderResult>}
77
87
  */
78
88
  export const renderWithEditor = async (
79
- ui: ReactElement,
89
+ ui: ReactElement | null,
80
90
  options?: EditorRenderOptions,
81
91
  ): Promise<EditorRenderResult> => {
92
+ const context = merge(defaultEditorContext, options?.context || {}) as EditorContextOptions;
82
93
  const result: Partial<EditorRenderResult> = {
83
94
  getHtmlContent: () => document.querySelector('.remirror-editor')?.innerHTML,
84
95
  getJsonContent: () => result.editor?.state.doc.content.child(0).toJSON(),
@@ -87,15 +98,17 @@ export const renderWithEditor = async (
87
98
  let isReady = false;
88
99
 
89
100
  const { container } = render(
90
- <TestEditor
91
- onReady={(manager) => {
92
- result.editor = RemirrorTestChain.create(manager);
93
- isReady = true;
94
- }}
95
- {...options}
96
- >
97
- {ui}
98
- </TestEditor>,
101
+ <EditorContext.Provider value={context}>
102
+ <TestEditor
103
+ onReady={(manager) => {
104
+ result.editor = RemirrorTestChain.create(manager);
105
+ isReady = true;
106
+ }}
107
+ {...options}
108
+ >
109
+ {ui}
110
+ </TestEditor>
111
+ </EditorContext.Provider>,
99
112
  );
100
113
 
101
114
  if (!isReady) {
package/tsconfig.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "strict": true,
12
12
  "forceConsistentCasingInFileNames": true,
13
13
  "module": "CommonJS",
14
- "moduleResolution": "Node",
14
+ "moduleResolution": "node",
15
15
  "resolveJsonModule": true,
16
16
  "isolatedModules": true,
17
17
  "jsx": "react",
@@ -1,2 +0,0 @@
1
- import Editor from './Editor/Editor';
2
- export default Editor;
@@ -1,7 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const Editor_1 = __importDefault(require("./Editor/Editor"));
7
- exports.default = Editor_1.default;