@squiz/formatted-text-editor 1.21.1-alpha.19 → 1.21.1-alpha.20
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/demo/App.tsx +31 -10
- package/demo/index.scss +2 -7
- package/lib/Editor/Editor.js +26 -2
- package/lib/Editor/EditorContext.d.ts +10 -0
- package/lib/Editor/EditorContext.js +15 -0
- package/lib/EditorToolbar/FloatingToolbar.js +15 -16
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +66 -14
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +11 -15
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -8
- package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
- package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
- package/lib/Extensions/Extensions.d.ts +6 -1
- package/lib/Extensions/Extensions.js +31 -20
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
- package/lib/Extensions/LinkExtension/LinkExtension.d.ts +21 -12
- package/lib/Extensions/LinkExtension/LinkExtension.js +63 -65
- package/lib/Extensions/LinkExtension/common.d.ts +7 -0
- package/lib/Extensions/LinkExtension/common.js +14 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useExpandedSelection.d.ts +23 -0
- package/lib/hooks/useExpandedSelection.js +37 -0
- package/lib/index.css +15 -3
- package/lib/index.d.ts +3 -2
- package/lib/index.js +5 -3
- package/lib/types.d.ts +3 -0
- package/lib/types.js +2 -0
- package/lib/ui/Button/Button.js +2 -3
- package/lib/ui/Fields/Input/Input.d.ts +1 -0
- package/lib/ui/Fields/Input/Input.js +8 -3
- package/lib/ui/Modal/Modal.js +2 -1
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +1 -2
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +110 -105
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +93 -69
- package/lib/utils/undefinedIfEmpty.d.ts +1 -0
- package/lib/utils/undefinedIfEmpty.js +7 -0
- package/package.json +3 -2
- package/src/Editor/Editor.spec.tsx +0 -26
- package/src/Editor/Editor.tsx +4 -3
- package/src/Editor/EditorContext.spec.tsx +26 -0
- package/src/Editor/EditorContext.ts +19 -0
- package/src/EditorToolbar/FloatingToolbar.tsx +19 -18
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +103 -25
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +16 -19
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +26 -26
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
- package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
- package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
- package/src/Extensions/Extensions.ts +31 -19
- package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
- package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
- package/src/Extensions/LinkExtension/LinkExtension.ts +88 -82
- package/src/Extensions/LinkExtension/common.ts +10 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useExpandedSelection.ts +44 -0
- package/src/index.ts +3 -2
- package/src/types.ts +5 -0
- package/src/ui/Button/Button.tsx +2 -4
- package/src/ui/Fields/Input/Input.tsx +18 -4
- package/src/ui/Modal/Modal.tsx +2 -1
- package/src/ui/_forms.scss +14 -0
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +177 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +41 -6
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +124 -113
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +56 -34
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +107 -79
- package/src/utils/undefinedIfEmpty.spec.ts +12 -0
- package/src/utils/undefinedIfEmpty.ts +3 -0
- package/tailwind.config.cjs +3 -0
- package/tests/renderWithEditor.tsx +21 -12
- package/lib/FormattedTextEditor.d.ts +0 -2
- package/lib/FormattedTextEditor.js +0 -7
- package/lib/utils/converters/validNodeTypes.d.ts +0 -2
- package/lib/utils/converters/validNodeTypes.js +0 -21
- package/src/Editor/Editor.mock.tsx +0 -43
- package/src/FormattedTextEditor.spec.tsx +0 -10
- package/src/FormattedTextEditor.tsx +0 -3
- package/src/utils/converters/validNodeTypes.spec.ts +0 -33
- package/src/utils/converters/validNodeTypes.ts +0 -21
@@ -1,14 +1,17 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.remirrorNodeToSquizNode = exports.resolveNodeTag = void 0;
|
4
|
-
const
|
4
|
+
const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
|
5
5
|
const resolveNodeTag = (node) => {
|
6
|
+
if (node.type.name === 'text') {
|
7
|
+
return 'span';
|
8
|
+
}
|
6
9
|
if (node.type.spec?.toDOM) {
|
7
10
|
const domNode = node.type.spec.toDOM(node);
|
8
11
|
if (domNode instanceof window.Node) {
|
9
12
|
return domNode.nodeName.toLowerCase();
|
10
13
|
}
|
11
|
-
if (domNode
|
14
|
+
if (typeof domNode === 'object' && 'dom' in domNode && domNode.dom instanceof window.Node) {
|
12
15
|
return domNode.dom.nodeName.toLowerCase();
|
13
16
|
}
|
14
17
|
if (domNode instanceof Array) {
|
@@ -16,7 +19,7 @@ const resolveNodeTag = (node) => {
|
|
16
19
|
return domNode[0].toLowerCase();
|
17
20
|
}
|
18
21
|
}
|
19
|
-
|
22
|
+
throw new Error('Unexpected Remirror node encountered, cannot resolve tag.');
|
20
23
|
};
|
21
24
|
exports.resolveNodeTag = resolveNodeTag;
|
22
25
|
const resolveFormattingOptions = (node) => {
|
@@ -39,122 +42,124 @@ const resolveFontOptions = (node) => {
|
|
39
42
|
case 'underline':
|
40
43
|
fontOptions.underline = true;
|
41
44
|
break;
|
42
|
-
default:
|
43
|
-
// Currently unsupported mark type
|
44
|
-
break;
|
45
45
|
}
|
46
46
|
});
|
47
47
|
return fontOptions;
|
48
48
|
};
|
49
|
-
const
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
node.marks.forEach((mark) => {
|
56
|
-
switch (mark.type.name) {
|
57
|
-
case 'link':
|
58
|
-
attributeOptions = { ...mark.attrs };
|
59
|
-
break;
|
60
|
-
default:
|
61
|
-
// Currently unsupported mark type
|
62
|
-
break;
|
63
|
-
}
|
64
|
-
});
|
65
|
-
}
|
66
|
-
// Remove any non string elements from attributes, squiz component only accepts strings.
|
67
|
-
Object.keys(attributeOptions).forEach((key) => {
|
68
|
-
if (typeof attributeOptions[key] !== 'string' && typeof attributeOptions[key] !== 'number') {
|
69
|
-
delete attributeOptions[key];
|
70
|
-
// If it's a number we make it a string so its accepted by component service
|
49
|
+
const transformAttributes = (attributes) => {
|
50
|
+
const transformed = {};
|
51
|
+
Object.keys(attributes).forEach((key) => {
|
52
|
+
// Component service requires attributes to be a string, cast as needed.
|
53
|
+
if (typeof attributes[key] === 'string' || typeof attributes[key] === 'number') {
|
54
|
+
transformed[key] = String(attributes[key]);
|
71
55
|
}
|
72
|
-
|
73
|
-
|
56
|
+
});
|
57
|
+
return transformed;
|
58
|
+
};
|
59
|
+
const transformFragment = (fragment) => {
|
60
|
+
const transformed = [];
|
61
|
+
fragment.forEach((child) => transformed.push(transformNode(child)));
|
62
|
+
return transformed;
|
63
|
+
};
|
64
|
+
const transformNode = (node) => {
|
65
|
+
const attributes = node.type.name === 'image' ? transformAttributes(node.attrs) : undefined;
|
66
|
+
const formattingOptions = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFormattingOptions(node));
|
67
|
+
const font = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFontOptions(node));
|
68
|
+
let transformedNode = { type: 'text', value: node.text || '' };
|
69
|
+
// Squiz "text" nodes can't have formatting/font options but Remirror "text" nodes can.
|
70
|
+
// If the node has formatting options wrap in a tag.
|
71
|
+
// If the node isn't a text type assume it is a tag type and wrap in a tag.
|
72
|
+
// If we pick the wrong tag here it will be corrected later as part of looping through the
|
73
|
+
// non-font marks.
|
74
|
+
if (node.type.name !== 'text' || attributes || formattingOptions || font) {
|
75
|
+
transformedNode = {
|
76
|
+
type: 'tag',
|
77
|
+
tag: (0, exports.resolveNodeTag)(node),
|
78
|
+
children: node.type.name === 'text' ? [transformedNode] : transformFragment(node.content),
|
79
|
+
attributes,
|
80
|
+
formattingOptions,
|
81
|
+
font,
|
82
|
+
};
|
83
|
+
}
|
84
|
+
node.marks.forEach((mark) => {
|
85
|
+
switch (mark.type.name) {
|
86
|
+
case 'bold':
|
87
|
+
case 'italic':
|
88
|
+
case 'underline':
|
89
|
+
break;
|
90
|
+
default:
|
91
|
+
transformedNode = transformMark(mark, transformedNode);
|
74
92
|
}
|
75
93
|
});
|
76
|
-
return
|
94
|
+
return transformedNode;
|
95
|
+
};
|
96
|
+
/**
|
97
|
+
* Merges 2 nodes together if they are compatible without losing any important details.
|
98
|
+
* Otherwise will wrap the node.
|
99
|
+
*
|
100
|
+
* @param {FormattedNode} node
|
101
|
+
* @param {FormattedNodeWithChildren} wrappingNode
|
102
|
+
*
|
103
|
+
* @return {FormattedNode}
|
104
|
+
*/
|
105
|
+
const wrapNodeIfNeeded = (node, wrappingNode) => {
|
106
|
+
if (node.type === 'tag' && wrappingNode.type === 'tag' && (node.tag === 'span' || node.tag === wrappingNode.tag)) {
|
107
|
+
// if the node we are wrapping with is a DOM node, and the node being wrapped is
|
108
|
+
// a plain looking DOM node merge the 2 nodes.
|
109
|
+
return {
|
110
|
+
...node,
|
111
|
+
...wrappingNode,
|
112
|
+
formattingOptions: (0, undefinedIfEmpty_1.undefinedIfEmpty)({
|
113
|
+
...node.formattingOptions,
|
114
|
+
...wrappingNode.formattingOptions,
|
115
|
+
}),
|
116
|
+
attributes: (0, undefinedIfEmpty_1.undefinedIfEmpty)({
|
117
|
+
...node.attributes,
|
118
|
+
...wrappingNode.attributes,
|
119
|
+
}),
|
120
|
+
font: (0, undefinedIfEmpty_1.undefinedIfEmpty)({
|
121
|
+
...node.font,
|
122
|
+
...wrappingNode.font,
|
123
|
+
}),
|
124
|
+
children: [...node.children, ...wrappingNode.children],
|
125
|
+
};
|
126
|
+
}
|
127
|
+
// if the node we are wrapping or the wrapping nodes are not compatible merge them.
|
128
|
+
return {
|
129
|
+
...wrappingNode,
|
130
|
+
children: [node, ...wrappingNode.children],
|
131
|
+
};
|
132
|
+
};
|
133
|
+
const transformMark = (mark, node) => {
|
134
|
+
switch (mark.type.name) {
|
135
|
+
case 'link':
|
136
|
+
return wrapNodeIfNeeded(node, {
|
137
|
+
type: 'tag',
|
138
|
+
tag: 'a',
|
139
|
+
attributes: transformAttributes(mark.attrs),
|
140
|
+
children: [],
|
141
|
+
});
|
142
|
+
case 'assetLink':
|
143
|
+
return wrapNodeIfNeeded(node, {
|
144
|
+
type: 'link-to-matrix-asset',
|
145
|
+
target: mark.attrs.target,
|
146
|
+
matrixIdentifier: mark.attrs.matrixIdentifier,
|
147
|
+
matrixDomain: mark.attrs.matrixDomain,
|
148
|
+
matrixAssetId: mark.attrs.matrixAssetId,
|
149
|
+
children: [],
|
150
|
+
});
|
151
|
+
}
|
152
|
+
throw new Error(`Unsupported mark "${mark.type.name}" was applied to node.`);
|
77
153
|
};
|
78
154
|
/**
|
79
155
|
* Converts Remirror node JSON structure to Squiz component JSON structure.
|
80
156
|
* @param {ProsemirrorNode} node Remirror node to convert to component.
|
81
|
-
* @export
|
82
157
|
* @returns {FormattedText} The converted Squiz component JSON.
|
83
158
|
*/
|
84
159
|
const remirrorNodeToSquizNode = (node) => {
|
85
|
-
if (
|
86
|
-
|
87
|
-
const nodeType = node.type.name;
|
88
|
-
let nodeTag = (0, exports.resolveNodeTag)(node);
|
89
|
-
// Filter out any children nodes that aren't currently supported.
|
90
|
-
const children = (node.content.content || []).map((child) => (0, exports.remirrorNodeToSquizNode)(child));
|
91
|
-
let transformed = {
|
92
|
-
children,
|
93
|
-
formattingOptions: resolveFormattingOptions(node),
|
94
|
-
attributes: resolveAttributeOptions(node, nodeType),
|
95
|
-
font: resolveFontOptions(node),
|
96
|
-
};
|
97
|
-
if (nodeType === 'doc') {
|
98
|
-
return transformed.children;
|
99
|
-
}
|
100
|
-
// If we don't have a node tag yet, check if there is one needed
|
101
|
-
if (!nodeTag) {
|
102
|
-
node.marks.forEach((mark) => {
|
103
|
-
switch (mark.type.name) {
|
104
|
-
case 'link':
|
105
|
-
nodeTag = 'a';
|
106
|
-
break;
|
107
|
-
default:
|
108
|
-
// Currently unsupported mark type
|
109
|
-
break;
|
110
|
-
}
|
111
|
-
});
|
112
|
-
}
|
113
|
-
if (nodeTag) {
|
114
|
-
transformed = {
|
115
|
-
...transformed,
|
116
|
-
type: 'tag',
|
117
|
-
tag: nodeTag,
|
118
|
-
};
|
119
|
-
}
|
120
|
-
if ((Object.keys(transformed.font).length > 0 || Object.keys(transformed.attributes).length > 0) &&
|
121
|
-
!transformed.type) {
|
122
|
-
// Wrap in span so we can apply formatting to it
|
123
|
-
transformed = { ...transformed, tag: 'span', type: 'tag' };
|
124
|
-
}
|
125
|
-
if (nodeType === 'text') {
|
126
|
-
if (transformed.type) {
|
127
|
-
// If we have a tag already nest the text beneath it so we can preserve formatting options, etc.
|
128
|
-
transformed = {
|
129
|
-
...transformed,
|
130
|
-
children: [
|
131
|
-
{
|
132
|
-
type: 'text',
|
133
|
-
value: node.text,
|
134
|
-
},
|
135
|
-
],
|
136
|
-
};
|
137
|
-
}
|
138
|
-
else {
|
139
|
-
// If we don't have a tag just rewrite the transformed value to be the text.
|
140
|
-
transformed = {
|
141
|
-
type: 'text',
|
142
|
-
value: node.text,
|
143
|
-
};
|
144
|
-
}
|
145
|
-
}
|
146
|
-
// Remove empty formatting options from transformed object.
|
147
|
-
if (transformed.formattingOptions && Object.keys(transformed.formattingOptions).length === 0) {
|
148
|
-
delete transformed.formattingOptions;
|
149
|
-
}
|
150
|
-
// Remove empty font options from transformed object.
|
151
|
-
if (transformed.font && Object.keys(transformed.font).length === 0) {
|
152
|
-
delete transformed.font;
|
160
|
+
if (node?.type?.name !== 'doc') {
|
161
|
+
throw new Error('Unable to convert from Remirror to Node data structure, unexpected node provided.');
|
153
162
|
}
|
154
|
-
|
155
|
-
if (transformed.attributes && Object.keys(transformed.attributes).length === 0) {
|
156
|
-
delete transformed.attributes;
|
157
|
-
}
|
158
|
-
return transformed;
|
163
|
+
return transformFragment(node.content);
|
159
164
|
};
|
160
165
|
exports.remirrorNodeToSquizNode = remirrorNodeToSquizNode;
|
@@ -1,8 +1,13 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.squizNodeToRemirrorNode = void 0;
|
4
|
+
const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
|
4
5
|
const getNodeType = (node) => {
|
5
|
-
const
|
6
|
+
const typeMap = {
|
7
|
+
'link-to-matrix-asset': 'text',
|
8
|
+
text: 'text',
|
9
|
+
};
|
10
|
+
const tagMap = {
|
6
11
|
h1: 'heading',
|
7
12
|
h2: 'heading',
|
8
13
|
h3: 'heading',
|
@@ -12,83 +17,98 @@ const getNodeType = (node) => {
|
|
12
17
|
img: 'image',
|
13
18
|
pre: 'preformatted',
|
14
19
|
p: 'paragraph',
|
15
|
-
|
20
|
+
a: 'text',
|
21
|
+
span: 'text',
|
16
22
|
};
|
17
|
-
|
23
|
+
if (typeMap[node.type]) {
|
24
|
+
return typeMap[node.type];
|
25
|
+
}
|
26
|
+
if (node.type === 'tag' && tagMap[node.tag]) {
|
27
|
+
return tagMap[node.tag];
|
28
|
+
}
|
18
29
|
// Unsupported node type
|
19
|
-
|
20
|
-
|
21
|
-
|
30
|
+
throw new Error(node.type === 'tag'
|
31
|
+
? `Unsupported node type provided: ${node.type} (tag: ${node.tag})`
|
32
|
+
: `Unsupported node type provided: ${node.type}`);
|
22
33
|
};
|
23
34
|
const getNodeAttributes = (node) => {
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
};
|
33
|
-
const resolveChild = (child) => {
|
34
|
-
if (child.type === 'text') {
|
35
|
-
return { type: 'text', text: child.value };
|
35
|
+
if (node.type === 'tag' && node.tag === 'img') {
|
36
|
+
return {
|
37
|
+
alt: node.attributes?.alt,
|
38
|
+
height: node.attributes?.height,
|
39
|
+
width: node.attributes?.width,
|
40
|
+
src: node.attributes?.src,
|
41
|
+
title: node.attributes?.title,
|
42
|
+
};
|
36
43
|
}
|
37
|
-
|
44
|
+
else if (node.type === 'tag') {
|
45
|
+
return {
|
46
|
+
nodeIndent: null,
|
47
|
+
nodeTextAlignment: node.formattingOptions?.alignment || null,
|
48
|
+
nodeLineHeight: null,
|
49
|
+
style: '',
|
50
|
+
level: node.tag?.startsWith('h') ? parseInt(node.tag.substring(1)) : undefined,
|
51
|
+
};
|
52
|
+
}
|
53
|
+
return {};
|
54
|
+
};
|
55
|
+
const getNodeMarks = (node) => {
|
38
56
|
const marks = [];
|
39
|
-
if (
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
57
|
+
if (node.type === 'tag' && node.tag === 'a') {
|
58
|
+
marks.push({
|
59
|
+
type: 'link',
|
60
|
+
attrs: {
|
61
|
+
href: node.attributes?.href,
|
62
|
+
target: node.attributes?.target ?? null,
|
63
|
+
auto: false,
|
64
|
+
title: node.attributes?.title ?? null,
|
65
|
+
},
|
66
|
+
});
|
67
|
+
}
|
68
|
+
else if (node.type === 'link-to-matrix-asset') {
|
69
|
+
marks.push({
|
70
|
+
type: 'assetLink',
|
71
|
+
attrs: {
|
72
|
+
matrixAssetId: node.matrixAssetId,
|
73
|
+
matrixDomain: node.matrixDomain,
|
74
|
+
matrixIdentifier: node.matrixIdentifier,
|
75
|
+
target: node.target,
|
76
|
+
},
|
77
|
+
});
|
78
|
+
}
|
79
|
+
// Handle font formatting
|
80
|
+
if ('font' in node) {
|
81
|
+
node.font?.bold && marks.push({ type: 'bold' });
|
82
|
+
node.font?.italics && marks.push({ type: 'italic' });
|
83
|
+
node.font?.underline && marks.push({ type: 'underline' });
|
84
|
+
}
|
85
|
+
return marks;
|
86
|
+
};
|
87
|
+
const unwrapNodeIfNeeded = (node) => {
|
88
|
+
if (node.type === 'text' && node.content?.length) {
|
89
|
+
return node.content.map((child) => {
|
54
90
|
return {
|
55
|
-
|
56
|
-
|
57
|
-
alt: child.attributes?.alt,
|
58
|
-
height: child.attributes?.height,
|
59
|
-
width: child.attributes?.width,
|
60
|
-
src: child.attributes?.src,
|
61
|
-
title: child.attributes?.title,
|
62
|
-
},
|
91
|
+
...child,
|
92
|
+
marks: [...(child.marks || []), ...(node.marks || [])],
|
63
93
|
};
|
64
|
-
}
|
65
|
-
// Handle font formatting
|
66
|
-
child.font?.bold && marks.push({ type: 'bold' });
|
67
|
-
child.font?.italics && marks.push({ type: 'italic' });
|
68
|
-
child.font?.underline && marks.push({ type: 'underline' });
|
69
|
-
// For now all children types should be "text"
|
70
|
-
text = child.children[0].type === 'text' ? child.children[0].value : '';
|
94
|
+
});
|
71
95
|
}
|
72
|
-
return
|
96
|
+
return [node];
|
73
97
|
};
|
74
98
|
const formatNode = (node) => {
|
75
|
-
|
76
|
-
if (
|
77
|
-
|
99
|
+
const children = [];
|
100
|
+
if ('children' in node) {
|
101
|
+
node.children.forEach((child) => {
|
102
|
+
children.push(...formatNode(child));
|
103
|
+
});
|
78
104
|
}
|
79
|
-
|
80
|
-
content = [
|
81
|
-
{
|
82
|
-
type: 'text',
|
83
|
-
text: node.value,
|
84
|
-
},
|
85
|
-
];
|
86
|
-
}
|
87
|
-
return {
|
105
|
+
return unwrapNodeIfNeeded({
|
88
106
|
type: getNodeType(node),
|
89
|
-
attrs: getNodeAttributes(node),
|
90
|
-
|
91
|
-
|
107
|
+
attrs: (0, undefinedIfEmpty_1.undefinedIfEmpty)(getNodeAttributes(node)),
|
108
|
+
marks: (0, undefinedIfEmpty_1.undefinedIfEmpty)(getNodeMarks(node)),
|
109
|
+
text: node.type === 'text' ? node.value : undefined,
|
110
|
+
content: (0, undefinedIfEmpty_1.undefinedIfEmpty)(children),
|
111
|
+
});
|
92
112
|
};
|
93
113
|
/**
|
94
114
|
* Converts Squiz component JSON structure to Remirror node JSON structure.
|
@@ -97,9 +117,13 @@ const formatNode = (node) => {
|
|
97
117
|
* @returns {RemirrorJSON} The converted Remirror JSON.
|
98
118
|
*/
|
99
119
|
const squizNodeToRemirrorNode = (nodes) => {
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
};
|
120
|
+
let children = [];
|
121
|
+
nodes.forEach((node) => {
|
122
|
+
children.push(...formatNode(node));
|
123
|
+
});
|
124
|
+
if (children.find((child) => child.type === 'text')) {
|
125
|
+
children = [{ type: 'paragraph', content: children }];
|
126
|
+
}
|
127
|
+
return { type: 'doc', content: children };
|
104
128
|
};
|
105
129
|
exports.squizNodeToRemirrorNode = squizNodeToRemirrorNode;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const undefinedIfEmpty: <T extends object>(object: T) => T | undefined;
|
@@ -0,0 +1,7 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.undefinedIfEmpty = void 0;
|
4
|
+
const undefinedIfEmpty = (object) => {
|
5
|
+
return Object.keys(object).length > 0 ? object : undefined;
|
6
|
+
};
|
7
|
+
exports.undefinedIfEmpty = undefinedIfEmpty;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/formatted-text-editor",
|
3
|
-
"version": "1.21.1-alpha.
|
3
|
+
"version": "1.21.1-alpha.20",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"scripts": {
|
@@ -36,6 +36,7 @@
|
|
36
36
|
"@vitejs/plugin-react": "3.0.0",
|
37
37
|
"autoprefixer": "10.4.13",
|
38
38
|
"cypress": "12.5.1",
|
39
|
+
"deepmerge": "^4.3.0",
|
39
40
|
"eslint-plugin-cypress": "2.12.1",
|
40
41
|
"eslint-plugin-jsx-a11y": "6.7.1",
|
41
42
|
"eslint-plugin-react": "7.32.2",
|
@@ -71,5 +72,5 @@
|
|
71
72
|
"volta": {
|
72
73
|
"node": "18.15.0"
|
73
74
|
},
|
74
|
-
"gitHead": "
|
75
|
+
"gitHead": "83f7b471dd6dd239c5b25233dcb5696fa96267e7"
|
75
76
|
}
|
@@ -1,14 +1,10 @@
|
|
1
|
-
import { jest } from '@jest/globals';
|
2
1
|
import React from 'react';
|
3
2
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
4
|
-
import { MockEditor } from './Editor.mock';
|
5
3
|
import Editor from './Editor';
|
6
4
|
import '@testing-library/jest-dom';
|
7
5
|
import { renderWithEditor } from '../../tests/renderWithEditor';
|
8
6
|
import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
|
9
7
|
|
10
|
-
const setContent: any = jest.fn();
|
11
|
-
|
12
8
|
describe('Formatted text editor', () => {
|
13
9
|
it('Renders the text editor', () => {
|
14
10
|
render(<Editor />);
|
@@ -232,28 +228,6 @@ describe('Formatted text editor', () => {
|
|
232
228
|
expect(baseElement.querySelectorAll('div.remirror-editor p')).toHaveLength(1);
|
233
229
|
});
|
234
230
|
|
235
|
-
it('Should allow text input & undo input upon clicking undo button', () => {
|
236
|
-
const { baseElement, getByLabelText } = render(<MockEditor setContent={setContent} />);
|
237
|
-
|
238
|
-
const textContent = `This is a string with a random number: ${Math.random() * 9999}`;
|
239
|
-
|
240
|
-
// This sets the content of the text editor
|
241
|
-
act(() => {
|
242
|
-
setContent(`<p>${textContent}</p>`, { triggerChange: true });
|
243
|
-
});
|
244
|
-
|
245
|
-
const editorNode = getByLabelText(`Text editor`);
|
246
|
-
expect(editorNode).toBeTruthy();
|
247
|
-
expect(editorNode.textContent).toBe(textContent);
|
248
|
-
|
249
|
-
// Testing if clicking undo button removes text from editor
|
250
|
-
const undoButton = baseElement.querySelector('button[title="Undo (cmd+Z)"]') as HTMLButtonElement;
|
251
|
-
expect(undoButton).toBeTruthy();
|
252
|
-
fireEvent.click(undoButton);
|
253
|
-
|
254
|
-
expect(editorNode.textContent).toBe('');
|
255
|
-
});
|
256
|
-
|
257
231
|
it('should allow text to be pasted into the editor', async () => {
|
258
232
|
const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
259
233
|
content: 'Some nonsense content here',
|
package/src/Editor/Editor.tsx
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
import React, { ClipboardEvent } from 'react';
|
1
|
+
import React, { ClipboardEvent, useContext } from 'react';
|
2
2
|
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
|
3
3
|
import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
|
4
4
|
import { Toolbar, FloatingToolbar } from '../EditorToolbar';
|
5
|
-
import {
|
5
|
+
import { EditorContext } from './EditorContext';
|
6
|
+
import { createExtensions } from '../Extensions/Extensions';
|
6
7
|
import clsx from 'clsx';
|
7
8
|
|
8
9
|
type EditorProps = {
|
@@ -13,7 +14,7 @@ type EditorProps = {
|
|
13
14
|
|
14
15
|
const Editor = ({ content, editable = true, onChange }: EditorProps) => {
|
15
16
|
const { manager, state, setState } = useRemirror({
|
16
|
-
extensions:
|
17
|
+
extensions: createExtensions(useContext(EditorContext)),
|
17
18
|
content,
|
18
19
|
selection: 'start',
|
19
20
|
stringHandler: 'html',
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import React, { useContext } from 'react';
|
2
|
+
import { EditorContext } from './EditorContext';
|
3
|
+
import { render } from '@testing-library/react';
|
4
|
+
|
5
|
+
describe('EditorContext', () => {
|
6
|
+
const defaultContextFn = jest.fn();
|
7
|
+
const Component = () => {
|
8
|
+
defaultContextFn(useContext(EditorContext));
|
9
|
+
return null;
|
10
|
+
};
|
11
|
+
|
12
|
+
it('Has expected defaults', () => {
|
13
|
+
render(<Component />);
|
14
|
+
|
15
|
+
const defaultContext = defaultContextFn.mock.calls[0][0];
|
16
|
+
|
17
|
+
expect(defaultContext).toEqual({
|
18
|
+
matrix: {
|
19
|
+
isValidMatrixAssetId: expect.any(Function),
|
20
|
+
matrixDomain: '',
|
21
|
+
matrixIdentifier: '',
|
22
|
+
},
|
23
|
+
});
|
24
|
+
expect(defaultContext.matrix.isValidMatrixAssetId('fake-asset-id')).toBe(true);
|
25
|
+
});
|
26
|
+
});
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
export type EditorContextOptions = {
|
4
|
+
matrix: {
|
5
|
+
matrixIdentifier: string;
|
6
|
+
matrixDomain: string;
|
7
|
+
isValidMatrixAssetId: (assetId: string) => boolean | Promise<boolean>;
|
8
|
+
};
|
9
|
+
};
|
10
|
+
|
11
|
+
export const defaultEditorContext: EditorContextOptions = {
|
12
|
+
matrix: {
|
13
|
+
matrixIdentifier: '',
|
14
|
+
matrixDomain: '',
|
15
|
+
isValidMatrixAssetId: () => true,
|
16
|
+
},
|
17
|
+
};
|
18
|
+
|
19
|
+
export const EditorContext = React.createContext(defaultEditorContext);
|
@@ -5,18 +5,21 @@ import BoldButton from './Tools/Bold/BoldButton';
|
|
5
5
|
import { useExtensionNames } from '../hooks';
|
6
6
|
import RemoveLinkButton from './Tools/Link/RemoveLinkButton';
|
7
7
|
import LinkButton from './Tools/Link/LinkButton';
|
8
|
-
import { FloatingToolbar as RemirrorFloatingToolbar,
|
8
|
+
import { FloatingToolbar as RemirrorFloatingToolbar, useActive, usePositioner } from '@remirror/react';
|
9
9
|
import { VerticalDivider } from '@remirror/react-components';
|
10
10
|
import { createToolbarPositioner, ToolbarPositionerRange } from '../utils/createToolbarPositioner';
|
11
11
|
import ImageButton from './Tools/Image/ImageButton';
|
12
|
+
import { MarkName } from '../Extensions/Extensions';
|
12
13
|
import { ImageExtension } from '../Extensions/ImageExtension/ImageExtension';
|
13
14
|
|
14
|
-
// The editor main toolbar
|
15
15
|
export const FloatingToolbar = () => {
|
16
|
+
const watchedMarks = [MarkName.Link, MarkName.AssetLink];
|
16
17
|
const extensionNames = useExtensionNames();
|
17
|
-
const positioner = useMemo(() => createToolbarPositioner({ types:
|
18
|
-
const
|
19
|
-
const
|
18
|
+
const positioner = useMemo(() => createToolbarPositioner({ types: watchedMarks }), []);
|
19
|
+
const active = useActive<ImageExtension>();
|
20
|
+
const {
|
21
|
+
data: { marks },
|
22
|
+
} = usePositioner<Partial<ToolbarPositionerRange>>(positioner, []);
|
20
23
|
|
21
24
|
let buttons = [
|
22
25
|
extensionNames.bold && <BoldButton key="bold" />,
|
@@ -24,23 +27,21 @@ export const FloatingToolbar = () => {
|
|
24
27
|
extensionNames.underline && <UnderlineButton key="underline" />,
|
25
28
|
];
|
26
29
|
|
27
|
-
if (
|
28
|
-
if (data.marks?.link.isExclusivelyActive) {
|
29
|
-
// if all of the selected text is a link show the options to update/remove the link instead of the regular
|
30
|
-
// formatting options.
|
31
|
-
buttons = [<LinkButton key="update-link" inPopover={true} />, <RemoveLinkButton key="remove-link" />];
|
32
|
-
} else if (!data.marks?.link.isActive) {
|
33
|
-
// if none of the selected text is a link show the option to create a link.
|
34
|
-
buttons.push(
|
35
|
-
<VerticalDivider key="link-divider" className="editor-divider" />,
|
36
|
-
<LinkButton key="add-link" inPopover={true} />,
|
37
|
-
);
|
38
|
-
}
|
39
|
-
} else {
|
30
|
+
if (active.image()) {
|
40
31
|
buttons.push(
|
41
32
|
<VerticalDivider key="image-divider" className="editor-divider" />,
|
42
33
|
<ImageButton key="add-image" inPopover={true} />,
|
43
34
|
);
|
35
|
+
} else if (marks?.[MarkName.Link].isExclusivelyActive || marks?.[MarkName.AssetLink].isExclusivelyActive) {
|
36
|
+
// if all of the selected text is a link show the options to update/remove the link instead of the regular
|
37
|
+
// formatting options.
|
38
|
+
buttons = [<LinkButton key="update-link" inPopover={true} />, <RemoveLinkButton key="remove-link" />];
|
39
|
+
} else if (!marks?.[MarkName.Link].isActive && !marks?.[MarkName.AssetLink].isActive) {
|
40
|
+
// if none of the selected text is a link show the option to create a link.
|
41
|
+
buttons.push(
|
42
|
+
<VerticalDivider key="link-divider" className="editor-divider" />,
|
43
|
+
<LinkButton key="add-link" inPopover={true} />,
|
44
|
+
);
|
44
45
|
}
|
45
46
|
|
46
47
|
return (
|