@squiz/formatted-text-editor 1.16.0 → 1.21.1-alpha.10
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/.eslintrc.json +7 -0
- package/README.md +10 -0
- package/demo/App.tsx +18 -4
- package/demo/index.scss +16 -10
- package/jest.config.ts +8 -9
- package/lib/Editor/Editor.js +18 -13
- package/lib/EditorToolbar/FloatingToolbar.js +50 -20
- package/lib/EditorToolbar/Toolbar.js +33 -24
- package/lib/EditorToolbar/Tools/Bold/BoldButton.js +14 -9
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +17 -0
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +84 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +2 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +67 -0
- package/lib/EditorToolbar/Tools/Image/ImageModal.d.ts +8 -0
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +19 -0
- package/lib/EditorToolbar/Tools/Italic/ItalicButton.js +14 -9
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +20 -15
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +42 -14
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +16 -11
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +13 -8
- package/lib/EditorToolbar/Tools/Redo/RedoButton.js +13 -8
- package/lib/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.js +13 -8
- package/lib/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.js +13 -8
- package/lib/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.js +13 -8
- package/lib/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.js +13 -8
- package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.js +19 -14
- package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.js +19 -14
- package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.js +14 -9
- package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +13 -8
- package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +24 -19
- package/lib/EditorToolbar/Tools/Underline/UnderlineButton.js +14 -9
- package/lib/EditorToolbar/Tools/Undo/UndoButton.js +13 -8
- package/lib/EditorToolbar/index.js +18 -2
- package/lib/Extensions/Extensions.d.ts +2 -4
- package/lib/Extensions/Extensions.js +19 -13
- package/lib/Extensions/ImageExtension/ImageExtension.d.ts +3 -0
- package/lib/Extensions/ImageExtension/ImageExtension.js +7 -0
- package/lib/Extensions/LinkExtension/LinkExtension.js +17 -11
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +10 -7
- package/lib/FormattedTextEditor.js +7 -2
- package/lib/hooks/index.js +17 -1
- package/lib/hooks/useExtensionNames.js +9 -5
- package/lib/index.css +133 -76
- package/lib/index.d.ts +3 -1
- package/lib/index.js +12 -2
- package/lib/ui/Button/Button.d.ts +11 -0
- package/lib/ui/Button/Button.js +14 -0
- package/lib/ui/Fields/Input/Input.d.ts +4 -0
- package/lib/ui/Fields/Input/Input.js +33 -0
- package/lib/ui/Fields/Select/Select.js +53 -0
- package/lib/ui/Modal/FormModal.js +33 -5
- package/lib/ui/Modal/Modal.js +50 -22
- package/lib/ui/ToolbarDropdown/ToolbarDropdown.js +38 -10
- package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +11 -6
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +10 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +160 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +105 -0
- package/lib/utils/converters/validNodeTypes.d.ts +2 -0
- package/lib/utils/converters/validNodeTypes.js +21 -0
- package/lib/utils/createToolbarPositioner.js +16 -12
- package/lib/utils/getCursorRect.js +5 -1
- package/package.json +7 -3
- package/src/Editor/_editor.scss +2 -49
- package/src/EditorToolbar/FloatingToolbar.tsx +1 -1
- package/src/EditorToolbar/Toolbar.tsx +2 -0
- package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +23 -0
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +92 -0
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +79 -0
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +57 -0
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +83 -0
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +29 -0
- package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +5 -5
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Redo/RedoButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Undo/UndoButton.tsx +2 -2
- package/src/EditorToolbar/_floating-toolbar.scss +6 -0
- package/src/EditorToolbar/_toolbar.scss +8 -2
- package/src/Extensions/Extensions.ts +5 -2
- package/src/Extensions/ImageExtension/ImageExtension.ts +3 -0
- package/src/Extensions/LinkExtension/LinkExtension.ts +8 -5
- package/src/index.scss +2 -2
- package/src/index.ts +3 -1
- package/src/ui/Button/Button.spec.tsx +44 -0
- package/src/ui/Button/Button.tsx +31 -0
- package/src/ui/{_buttons.scss → Button/_button.scss} +19 -1
- package/src/ui/{Inputs/Text/TextInput.spec.tsx → Fields/Input/Input.spec.tsx} +8 -8
- package/src/ui/{Inputs/Text/TextInput.tsx → Fields/Input/Input.tsx} +4 -4
- package/src/ui/Modal/Modal.tsx +1 -0
- package/src/ui/ToolbarDropdown/ToolbarDropdown.spec.tsx +4 -2
- package/src/ui/ToolbarDropdown/ToolbarDropdown.tsx +1 -1
- package/src/ui/_typography.scss +46 -0
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +75 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +445 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +191 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +307 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +123 -0
- package/src/utils/converters/validNodeTypes.spec.ts +33 -0
- package/src/utils/converters/validNodeTypes.ts +21 -0
- package/tests/renderWithEditor.tsx +2 -2
- package/tsconfig.json +1 -1
- package/lib/ui/Inputs/Select/Select.js +0 -23
- package/lib/ui/Inputs/Text/TextInput.d.ts +0 -4
- package/lib/ui/Inputs/Text/TextInput.js +0 -7
- package/lib/ui/ToolbarButton/ToolbarButton.d.ts +0 -10
- package/lib/ui/ToolbarButton/ToolbarButton.js +0 -5
- package/src/ui/ToolbarButton/ToolbarButton.tsx +0 -26
- package/src/ui/ToolbarButton/_toolbar-button.scss +0 -17
- /package/lib/ui/{Inputs → Fields}/Select/Select.d.ts +0 -0
- /package/src/ui/{Inputs → Fields}/Select/Select.spec.tsx +0 -0
- /package/src/ui/{Inputs → Fields}/Select/Select.tsx +0 -0
@@ -0,0 +1,307 @@
|
|
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 { mockSquizNodeJson, mockSquizNodeTextJson } from '../mocks/squizNodeJson.mock';
|
5
|
+
|
6
|
+
type FormattedText = FormattedTextModels.v1.FormattedText;
|
7
|
+
|
8
|
+
describe('squizNodeToRemirrorNode', () => {
|
9
|
+
it('should convert complex Squiz component JSON to Remirror JSON', () => {
|
10
|
+
expect(squizNodeToRemirrorNode(mockSquizNodeJson)).toEqual({
|
11
|
+
type: 'doc',
|
12
|
+
content: [
|
13
|
+
{
|
14
|
+
type: 'paragraph',
|
15
|
+
attrs: {
|
16
|
+
nodeIndent: null,
|
17
|
+
nodeTextAlignment: null,
|
18
|
+
nodeLineHeight: null,
|
19
|
+
style: '',
|
20
|
+
},
|
21
|
+
content: [
|
22
|
+
{
|
23
|
+
type: 'text',
|
24
|
+
text: 'Hello ',
|
25
|
+
},
|
26
|
+
{
|
27
|
+
type: 'text',
|
28
|
+
marks: [
|
29
|
+
{
|
30
|
+
type: 'link',
|
31
|
+
attrs: {
|
32
|
+
href: 'https://www.google.com',
|
33
|
+
target: null,
|
34
|
+
auto: false,
|
35
|
+
title: null,
|
36
|
+
},
|
37
|
+
},
|
38
|
+
{
|
39
|
+
type: 'bold',
|
40
|
+
},
|
41
|
+
],
|
42
|
+
text: 'Mr Bean',
|
43
|
+
},
|
44
|
+
{
|
45
|
+
type: 'text',
|
46
|
+
text: ', nice to ',
|
47
|
+
},
|
48
|
+
{
|
49
|
+
type: 'text',
|
50
|
+
marks: [
|
51
|
+
{
|
52
|
+
type: 'link',
|
53
|
+
attrs: {
|
54
|
+
href: 'https://www.google.com',
|
55
|
+
target: null,
|
56
|
+
auto: false,
|
57
|
+
title: null,
|
58
|
+
},
|
59
|
+
},
|
60
|
+
],
|
61
|
+
text: 'meet you',
|
62
|
+
},
|
63
|
+
{
|
64
|
+
type: 'text',
|
65
|
+
text: '.',
|
66
|
+
},
|
67
|
+
{
|
68
|
+
type: 'image',
|
69
|
+
attrs: {
|
70
|
+
alt: 'Test',
|
71
|
+
height: '150',
|
72
|
+
width: '200',
|
73
|
+
src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
|
74
|
+
title: '',
|
75
|
+
},
|
76
|
+
},
|
77
|
+
],
|
78
|
+
},
|
79
|
+
],
|
80
|
+
});
|
81
|
+
});
|
82
|
+
|
83
|
+
it('should convert top level text component JSON to Remirror JSON', () => {
|
84
|
+
expect(squizNodeToRemirrorNode(mockSquizNodeTextJson)).toEqual({
|
85
|
+
content: [
|
86
|
+
{
|
87
|
+
attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
|
88
|
+
type: 'paragraph',
|
89
|
+
content: [
|
90
|
+
{
|
91
|
+
type: 'text',
|
92
|
+
text: 'Hello world!',
|
93
|
+
},
|
94
|
+
],
|
95
|
+
},
|
96
|
+
{
|
97
|
+
attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
|
98
|
+
type: 'paragraph',
|
99
|
+
content: [
|
100
|
+
{
|
101
|
+
type: 'text',
|
102
|
+
text: 'Another one...',
|
103
|
+
},
|
104
|
+
],
|
105
|
+
},
|
106
|
+
],
|
107
|
+
type: 'doc',
|
108
|
+
});
|
109
|
+
});
|
110
|
+
|
111
|
+
it('should handle empty Squiz component JSON', () => {
|
112
|
+
expect(squizNodeToRemirrorNode([])).toEqual({ content: [], type: 'doc' });
|
113
|
+
expect(
|
114
|
+
squizNodeToRemirrorNode([
|
115
|
+
{
|
116
|
+
children: [],
|
117
|
+
type: 'tag',
|
118
|
+
tag: 'p',
|
119
|
+
},
|
120
|
+
]),
|
121
|
+
).toEqual({
|
122
|
+
content: [
|
123
|
+
{
|
124
|
+
attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
|
125
|
+
type: 'paragraph',
|
126
|
+
},
|
127
|
+
],
|
128
|
+
type: 'doc',
|
129
|
+
});
|
130
|
+
});
|
131
|
+
|
132
|
+
it('should throw an error for non supported node types', () => {
|
133
|
+
const squizComponentJSON: FormattedText = [
|
134
|
+
{
|
135
|
+
children: [
|
136
|
+
{
|
137
|
+
type: 'text',
|
138
|
+
value: 'Hello world!',
|
139
|
+
},
|
140
|
+
],
|
141
|
+
type: 'tag',
|
142
|
+
tag: 'p',
|
143
|
+
},
|
144
|
+
// This should be filtered out, as we don't currently support <code> tags
|
145
|
+
{
|
146
|
+
children: [
|
147
|
+
{
|
148
|
+
type: 'text',
|
149
|
+
value: 'Should be filtered out...',
|
150
|
+
},
|
151
|
+
],
|
152
|
+
type: 'tag',
|
153
|
+
tag: 'code',
|
154
|
+
},
|
155
|
+
];
|
156
|
+
|
157
|
+
expect(() => squizNodeToRemirrorNode(squizComponentJSON)).toThrow(`Unsupported node type provided: code`);
|
158
|
+
});
|
159
|
+
|
160
|
+
it('should handle pre formatted text', () => {
|
161
|
+
const squizComponentJSON: FormattedText = [
|
162
|
+
{
|
163
|
+
children: [
|
164
|
+
{
|
165
|
+
type: 'text',
|
166
|
+
value: 'Hello world!',
|
167
|
+
},
|
168
|
+
],
|
169
|
+
type: 'tag',
|
170
|
+
tag: 'pre',
|
171
|
+
},
|
172
|
+
];
|
173
|
+
|
174
|
+
const expected: RemirrorJSON = {
|
175
|
+
content: [
|
176
|
+
{
|
177
|
+
attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
|
178
|
+
content: [{ text: 'Hello world!', type: 'text' }],
|
179
|
+
type: 'preformatted',
|
180
|
+
},
|
181
|
+
],
|
182
|
+
type: 'doc',
|
183
|
+
};
|
184
|
+
|
185
|
+
const result = squizNodeToRemirrorNode(squizComponentJSON);
|
186
|
+
expect(result).toEqual(expected);
|
187
|
+
});
|
188
|
+
|
189
|
+
it('should handle images', () => {
|
190
|
+
const squizComponentJSON: FormattedText = [
|
191
|
+
{
|
192
|
+
children: [
|
193
|
+
{
|
194
|
+
children: [],
|
195
|
+
attributes: {
|
196
|
+
alt: 'This is a test alt',
|
197
|
+
height: '360',
|
198
|
+
width: '480',
|
199
|
+
src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
|
200
|
+
title: '',
|
201
|
+
},
|
202
|
+
type: 'tag',
|
203
|
+
tag: 'img',
|
204
|
+
},
|
205
|
+
],
|
206
|
+
type: 'tag',
|
207
|
+
tag: 'p',
|
208
|
+
},
|
209
|
+
];
|
210
|
+
|
211
|
+
const expected: RemirrorJSON = {
|
212
|
+
content: [
|
213
|
+
{
|
214
|
+
attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
|
215
|
+
content: [
|
216
|
+
{
|
217
|
+
type: 'image',
|
218
|
+
attrs: {
|
219
|
+
alt: 'This is a test alt',
|
220
|
+
height: '360',
|
221
|
+
width: '480',
|
222
|
+
src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
|
223
|
+
title: '',
|
224
|
+
},
|
225
|
+
},
|
226
|
+
],
|
227
|
+
type: 'paragraph',
|
228
|
+
},
|
229
|
+
],
|
230
|
+
type: 'doc',
|
231
|
+
};
|
232
|
+
|
233
|
+
const result = squizNodeToRemirrorNode(squizComponentJSON);
|
234
|
+
expect(result).toEqual(expected);
|
235
|
+
});
|
236
|
+
|
237
|
+
it.each([
|
238
|
+
['italics', 'italic'],
|
239
|
+
['bold', 'bold'],
|
240
|
+
['underline', 'underline'],
|
241
|
+
])('should handle %s formatting', (a, b) => {
|
242
|
+
const squizComponentJSON: FormattedText = [
|
243
|
+
{
|
244
|
+
children: [
|
245
|
+
{
|
246
|
+
children: [
|
247
|
+
{
|
248
|
+
type: 'text',
|
249
|
+
value: 'Hello world!',
|
250
|
+
},
|
251
|
+
],
|
252
|
+
font: {
|
253
|
+
[a]: true,
|
254
|
+
},
|
255
|
+
tag: 'span',
|
256
|
+
type: 'tag',
|
257
|
+
},
|
258
|
+
],
|
259
|
+
type: 'tag',
|
260
|
+
tag: 'p',
|
261
|
+
},
|
262
|
+
];
|
263
|
+
|
264
|
+
const expected: RemirrorJSON = {
|
265
|
+
content: [
|
266
|
+
{
|
267
|
+
attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
|
268
|
+
content: [{ text: 'Hello world!', marks: [{ type: b }], type: 'text' }],
|
269
|
+
type: 'paragraph',
|
270
|
+
},
|
271
|
+
],
|
272
|
+
type: 'doc',
|
273
|
+
};
|
274
|
+
|
275
|
+
const result = squizNodeToRemirrorNode(squizComponentJSON);
|
276
|
+
expect(result).toEqual(expected);
|
277
|
+
});
|
278
|
+
|
279
|
+
it.each([1, 2, 3, 4, 5, 6])('should handle heading %s and set it to level %s', (level) => {
|
280
|
+
const squizComponentJSON: FormattedText = [
|
281
|
+
{
|
282
|
+
children: [
|
283
|
+
{
|
284
|
+
type: 'text',
|
285
|
+
value: 'Hello world!',
|
286
|
+
},
|
287
|
+
],
|
288
|
+
type: 'tag',
|
289
|
+
tag: `h${level}`,
|
290
|
+
},
|
291
|
+
];
|
292
|
+
|
293
|
+
const expected: RemirrorJSON = {
|
294
|
+
content: [
|
295
|
+
{
|
296
|
+
attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '', level },
|
297
|
+
content: [{ text: 'Hello world!', type: 'text' }],
|
298
|
+
type: 'heading',
|
299
|
+
},
|
300
|
+
],
|
301
|
+
type: 'doc',
|
302
|
+
};
|
303
|
+
|
304
|
+
const result = squizNodeToRemirrorNode(squizComponentJSON);
|
305
|
+
expect(result).toEqual(expected);
|
306
|
+
});
|
307
|
+
});
|
@@ -0,0 +1,123 @@
|
|
1
|
+
import { RemirrorJSON, Literal, ObjectMark } from '@remirror/core';
|
2
|
+
import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
|
3
|
+
|
4
|
+
type FormattedText = FormattedTextModels.v1.FormattedText;
|
5
|
+
type FormattedTextTag = FormattedTextModels.v1.FormattedTextTag;
|
6
|
+
type FormattedNodes = FormattedTextModels.v1.FormattedNodes;
|
7
|
+
|
8
|
+
const getNodeType = (node: FormattedTextTag): string => {
|
9
|
+
const nodeTypeMap: Record<string, string> = {
|
10
|
+
h1: 'heading',
|
11
|
+
h2: 'heading',
|
12
|
+
h3: 'heading',
|
13
|
+
h4: 'heading',
|
14
|
+
h5: 'heading',
|
15
|
+
h6: 'heading',
|
16
|
+
img: 'image',
|
17
|
+
pre: 'preformatted',
|
18
|
+
p: 'paragraph',
|
19
|
+
text: 'paragraph',
|
20
|
+
};
|
21
|
+
|
22
|
+
const nodeType = nodeTypeMap[node.tag || node.type];
|
23
|
+
|
24
|
+
// Unsupported node type
|
25
|
+
if (!nodeType) throw new Error(`Unsupported node type provided: ${node.tag}`);
|
26
|
+
|
27
|
+
return nodeType;
|
28
|
+
};
|
29
|
+
|
30
|
+
const getNodeAttributes = (node: FormattedTextTag): Record<string, Literal> => {
|
31
|
+
const { alignment } = node.formattingOptions || {};
|
32
|
+
return {
|
33
|
+
nodeIndent: null,
|
34
|
+
nodeTextAlignment: alignment ?? null,
|
35
|
+
nodeLineHeight: null,
|
36
|
+
style: '',
|
37
|
+
level: node.tag?.startsWith('h') ? parseInt(node.tag.substring(1)) : undefined,
|
38
|
+
};
|
39
|
+
};
|
40
|
+
|
41
|
+
const resolveChild = (child: FormattedNodes): RemirrorJSON => {
|
42
|
+
if (child.type === 'text') {
|
43
|
+
return { type: 'text', text: child.value };
|
44
|
+
}
|
45
|
+
|
46
|
+
let text = '';
|
47
|
+
const marks: ObjectMark[] = [];
|
48
|
+
|
49
|
+
if (child.type === 'tag') {
|
50
|
+
// Handle link type
|
51
|
+
if (child.tag === 'a') {
|
52
|
+
marks.push({
|
53
|
+
type: 'link',
|
54
|
+
attrs: {
|
55
|
+
href: child.attributes?.href,
|
56
|
+
target: child.attributes?.target ?? null,
|
57
|
+
auto: false,
|
58
|
+
title: child.attributes?.title ?? null,
|
59
|
+
},
|
60
|
+
});
|
61
|
+
}
|
62
|
+
|
63
|
+
// Handle image type
|
64
|
+
if (child.tag === 'img') {
|
65
|
+
return {
|
66
|
+
type: 'image',
|
67
|
+
attrs: {
|
68
|
+
alt: child.attributes?.alt,
|
69
|
+
height: child.attributes?.height,
|
70
|
+
width: child.attributes?.width,
|
71
|
+
src: child.attributes?.src,
|
72
|
+
title: child.attributes?.title,
|
73
|
+
},
|
74
|
+
};
|
75
|
+
}
|
76
|
+
|
77
|
+
// Handle font formatting
|
78
|
+
child.font?.bold && marks.push({ type: 'bold' });
|
79
|
+
child.font?.italics && marks.push({ type: 'italic' });
|
80
|
+
child.font?.underline && marks.push({ type: 'underline' });
|
81
|
+
|
82
|
+
// For now all children types should be "text"
|
83
|
+
text = child.children[0].type === 'text' ? child.children[0].value : '';
|
84
|
+
}
|
85
|
+
|
86
|
+
return { type: 'text', marks, text };
|
87
|
+
};
|
88
|
+
|
89
|
+
const formatNode = (node: FormattedNodes): RemirrorJSON => {
|
90
|
+
let content: RemirrorJSON[] | undefined;
|
91
|
+
|
92
|
+
if (node.type === 'tag') {
|
93
|
+
content = node.children.length ? node.children.map((child: FormattedNodes) => resolveChild(child)) : undefined;
|
94
|
+
}
|
95
|
+
|
96
|
+
if (node.type === 'text') {
|
97
|
+
content = [
|
98
|
+
{
|
99
|
+
type: 'text',
|
100
|
+
text: node.value,
|
101
|
+
},
|
102
|
+
];
|
103
|
+
}
|
104
|
+
|
105
|
+
return {
|
106
|
+
type: getNodeType(node as FormattedTextTag),
|
107
|
+
attrs: getNodeAttributes(node as FormattedTextTag),
|
108
|
+
content,
|
109
|
+
};
|
110
|
+
};
|
111
|
+
|
112
|
+
/**
|
113
|
+
* Converts Squiz component JSON structure to Remirror node JSON structure.
|
114
|
+
* @param {FormattedText} nodes Squiz nodes to convert to Remirror.
|
115
|
+
* @export
|
116
|
+
* @returns {RemirrorJSON} The converted Remirror JSON.
|
117
|
+
*/
|
118
|
+
export const squizNodeToRemirrorNode = (nodes: FormattedText): RemirrorJSON => {
|
119
|
+
return {
|
120
|
+
type: 'doc',
|
121
|
+
content: nodes.filter((node: FormattedNodes) => getNodeType(node as FormattedTextTag)).map(formatNode),
|
122
|
+
};
|
123
|
+
};
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import { validRemirrorNode } from './validNodeTypes';
|
2
|
+
|
3
|
+
describe('validRemirrorNode', () => {
|
4
|
+
it('returns false for null input', () => {
|
5
|
+
expect(validRemirrorNode(null as any)).toBe(false);
|
6
|
+
});
|
7
|
+
|
8
|
+
it('returns false for unsupported node type', () => {
|
9
|
+
const node = { type: { name: 'unsupported' }, marks: [] };
|
10
|
+
expect(validRemirrorNode(node as any)).toBe(false);
|
11
|
+
});
|
12
|
+
|
13
|
+
it('returns false for unsupported mark type', () => {
|
14
|
+
const node = {
|
15
|
+
type: { name: 'doc' },
|
16
|
+
marks: [{ type: { name: 'unsupported' } }],
|
17
|
+
};
|
18
|
+
expect(validRemirrorNode(node as any)).toBe(false);
|
19
|
+
});
|
20
|
+
|
21
|
+
it('returns true for supported node type with no marks', () => {
|
22
|
+
const node = { type: { name: 'doc' }, marks: [] };
|
23
|
+
expect(validRemirrorNode(node as any)).toBe(true);
|
24
|
+
});
|
25
|
+
|
26
|
+
it('returns true for supported node type with supported mark type', () => {
|
27
|
+
const node = {
|
28
|
+
type: { name: 'doc' },
|
29
|
+
marks: [{ type: { name: 'bold' } }],
|
30
|
+
};
|
31
|
+
expect(validRemirrorNode(node as any)).toBe(true);
|
32
|
+
});
|
33
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { ProsemirrorNode } from 'remirror';
|
2
|
+
import { Extensions } from '../../Extensions/Extensions';
|
3
|
+
|
4
|
+
export const validRemirrorNode = (node: ProsemirrorNode): boolean => {
|
5
|
+
if (!node) return false;
|
6
|
+
|
7
|
+
const nodeType = node.type.name;
|
8
|
+
const nodeMarks = node.marks;
|
9
|
+
|
10
|
+
// This is pulling in the currently supported extensions, this works for now...
|
11
|
+
// Could also just hard code these in as we go, but this should make it easier as we add more extensions
|
12
|
+
const supportedNodes: Array<string> = [...Extensions().map((extension: any) => extension.name), 'doc', 'text'];
|
13
|
+
|
14
|
+
if (!supportedNodes.includes(nodeType)) return false;
|
15
|
+
|
16
|
+
for (let i = 0; i < nodeMarks.length; i++) {
|
17
|
+
if (!supportedNodes.includes(nodeMarks[i].type.name)) return false;
|
18
|
+
}
|
19
|
+
|
20
|
+
return true;
|
21
|
+
};
|
@@ -13,7 +13,7 @@ export type EditorRenderOptions = RenderOptions & {
|
|
13
13
|
};
|
14
14
|
|
15
15
|
type TestEditorProps = EditorRenderOptions & {
|
16
|
-
children: ReactElement;
|
16
|
+
children: ReactElement | null;
|
17
17
|
onReady: (manager: RemirrorManager<Extension>) => void;
|
18
18
|
};
|
19
19
|
|
@@ -76,7 +76,7 @@ const TestEditor = ({ children, extensions, content, onReady }: TestEditorProps)
|
|
76
76
|
* @return {Promise<EditorRenderResult>}
|
77
77
|
*/
|
78
78
|
export const renderWithEditor = async (
|
79
|
-
ui: ReactElement,
|
79
|
+
ui: ReactElement | null,
|
80
80
|
options?: EditorRenderOptions,
|
81
81
|
): Promise<EditorRenderResult> => {
|
82
82
|
const result: Partial<EditorRenderResult> = {
|
package/tsconfig.json
CHANGED
@@ -1,23 +0,0 @@
|
|
1
|
-
import { Listbox } from '@headlessui/react';
|
2
|
-
import React, { useState } from 'react';
|
3
|
-
import clsx from 'clsx';
|
4
|
-
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
|
5
|
-
import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded';
|
6
|
-
export const Select = ({ name, label, value, onChange, options }) => {
|
7
|
-
const [selectedOption, setSelectedOptions] = useState(value ? options[value] : null);
|
8
|
-
const handleChange = (value) => {
|
9
|
-
setSelectedOptions(options[value]);
|
10
|
-
onChange?.(value);
|
11
|
-
};
|
12
|
-
return (React.createElement(Listbox, { value: value, name: name, "aria-labelledby": name, onChange: handleChange }, ({ open }) => (React.createElement(React.Fragment, null,
|
13
|
-
label && React.createElement(Listbox.Label, { className: "squiz-fte-form-label" }, label),
|
14
|
-
React.createElement("div", { className: "relative text-md text-gray-800" },
|
15
|
-
React.createElement(Listbox.Button, { className: "w-full cursor-default rounded border-2 border-gray-300 bg-white py-2 pl-3 pr-10 focus:border-blue-300 focus:outline-none" },
|
16
|
-
React.createElement("span", { className: "flex items-center" },
|
17
|
-
React.createElement("span", { className: "block truncate" },
|
18
|
-
selectedOption?.label,
|
19
|
-
"\u00A0")),
|
20
|
-
React.createElement("span", { className: "pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2" }, !open ? (React.createElement(ExpandMoreRoundedIcon, { className: "text-gray-500", "aria-hidden": "true" })) : (React.createElement(ExpandLessRoundedIcon, { className: "text-gray-500", "aria-hidden": "true" })))),
|
21
|
-
React.createElement(Listbox.Options, { className: "absolute z-10 mt-1 w-full overflow-auto rounded border-2 border-gray-300 bg-white" }, Object.entries(options).map(([key, option]) => (React.createElement(Listbox.Option, { key: key, value: key, className: ({ active }) => clsx(active ? 'bg-gray-100' : 'bg-white', 'relative cursor-default select-none flex hover:bg-gray-100 py-2 px-3') },
|
22
|
-
React.createElement("span", { className: "block truncate" }, option.label))))))))));
|
23
|
-
};
|
@@ -1,7 +0,0 @@
|
|
1
|
-
import React, { forwardRef } from 'react';
|
2
|
-
const TextInputInternal = ({ name, label, ...rest }, ref) => {
|
3
|
-
return (React.createElement(React.Fragment, null,
|
4
|
-
label && (React.createElement("label", { htmlFor: name, className: "squiz-fte-form-label" }, label)),
|
5
|
-
React.createElement("input", { ref: ref, id: name, name: name, type: "text", className: "squiz-fte-form-control", ...rest })));
|
6
|
-
};
|
7
|
-
export const TextInput = forwardRef(TextInputInternal);
|
@@ -1,10 +0,0 @@
|
|
1
|
-
import { ReactElement } from 'react';
|
2
|
-
type ToolbarButtonProps = {
|
3
|
-
handleOnClick: () => void;
|
4
|
-
isDisabled?: boolean;
|
5
|
-
isActive: boolean;
|
6
|
-
icon: ReactElement;
|
7
|
-
label: string;
|
8
|
-
};
|
9
|
-
declare const ToolbarButton: ({ handleOnClick, isDisabled, isActive, icon, label }: ToolbarButtonProps) => JSX.Element;
|
10
|
-
export default ToolbarButton;
|
@@ -1,5 +0,0 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
const ToolbarButton = ({ handleOnClick, isDisabled, isActive, icon, label }) => {
|
3
|
-
return (React.createElement("button", { "aria-label": label, title: label, type: "button", onClick: handleOnClick, disabled: isDisabled, className: `squiz-fte-btn toolbar-button ${isActive ? 'is-active' : ''}` }, icon));
|
4
|
-
};
|
5
|
-
export default ToolbarButton;
|
@@ -1,26 +0,0 @@
|
|
1
|
-
import React, { ReactElement } from 'react';
|
2
|
-
|
3
|
-
type ToolbarButtonProps = {
|
4
|
-
handleOnClick: () => void;
|
5
|
-
isDisabled?: boolean;
|
6
|
-
isActive: boolean;
|
7
|
-
icon: ReactElement;
|
8
|
-
label: string;
|
9
|
-
};
|
10
|
-
|
11
|
-
const ToolbarButton = ({ handleOnClick, isDisabled, isActive, icon, label }: ToolbarButtonProps) => {
|
12
|
-
return (
|
13
|
-
<button
|
14
|
-
aria-label={label}
|
15
|
-
title={label}
|
16
|
-
type="button"
|
17
|
-
onClick={handleOnClick}
|
18
|
-
disabled={isDisabled}
|
19
|
-
className={`squiz-fte-btn toolbar-button ${isActive ? 'is-active' : ''}`}
|
20
|
-
>
|
21
|
-
{icon}
|
22
|
-
</button>
|
23
|
-
);
|
24
|
-
};
|
25
|
-
|
26
|
-
export default ToolbarButton;
|
@@ -1,17 +0,0 @@
|
|
1
|
-
.toolbar-button {
|
2
|
-
@apply bg-white text-gray-600 p-1;
|
3
|
-
|
4
|
-
~ .toolbar-button {
|
5
|
-
margin-left: 2px;
|
6
|
-
}
|
7
|
-
|
8
|
-
&:hover,
|
9
|
-
&:focus {
|
10
|
-
background-color: rgba(black, 0.04);
|
11
|
-
}
|
12
|
-
|
13
|
-
&.is-active,
|
14
|
-
&:active {
|
15
|
-
@apply text-blue-300 bg-blue-100;
|
16
|
-
}
|
17
|
-
}
|
File without changes
|
File without changes
|
File without changes
|