@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.
- package/demo/App.tsx +52 -10
- package/demo/index.scss +11 -10
- package/jest.config.ts +0 -2
- package/lib/Editor/Editor.js +45 -7
- package/lib/Editor/EditorContext.d.ts +15 -0
- package/lib/Editor/EditorContext.js +15 -0
- package/lib/EditorToolbar/FloatingToolbar.js +11 -5
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +91 -23
- package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +4 -1
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +22 -14
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +9 -5
- 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 +21 -13
- 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 +12 -5
- package/lib/Extensions/Extensions.js +42 -20
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
- package/lib/Extensions/ImageExtension/ImageExtension.d.ts +4 -0
- package/lib/Extensions/ImageExtension/ImageExtension.js +11 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
- package/lib/Extensions/LinkExtension/LinkExtension.d.ts +19 -12
- package/lib/Extensions/LinkExtension/LinkExtension.js +56 -66
- package/lib/Extensions/LinkExtension/common.d.ts +7 -0
- package/lib/Extensions/LinkExtension/common.js +14 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +6 -2
- 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 +58 -23
- package/lib/index.d.ts +5 -2
- package/lib/index.js +9 -3
- package/lib/types.d.ts +3 -0
- package/lib/types.js +2 -0
- package/lib/ui/Button/Button.d.ts +2 -1
- package/lib/ui/Button/Button.js +4 -5
- package/lib/ui/Fields/Input/Input.d.ts +1 -0
- package/lib/ui/Fields/Input/Input.js +9 -3
- package/lib/ui/Modal/Modal.js +5 -3
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +174 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +138 -0
- package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
- package/lib/utils/resolveMatrixAssetUrl.js +10 -0
- package/lib/utils/undefinedIfEmpty.d.ts +1 -0
- package/lib/utils/undefinedIfEmpty.js +7 -0
- package/package.json +10 -4
- package/src/Editor/Editor.spec.tsx +78 -18
- package/src/Editor/Editor.tsx +28 -9
- package/src/Editor/EditorContext.spec.tsx +26 -0
- package/src/Editor/EditorContext.ts +26 -0
- package/src/Editor/_editor.scss +20 -4
- package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
- package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +250 -2
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +59 -20
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +12 -10
- 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 +137 -26
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
- package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
- package/src/EditorToolbar/_floating-toolbar.scss +4 -5
- package/src/EditorToolbar/_toolbar.scss +1 -1
- package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
- package/src/Extensions/Extensions.ts +42 -18
- package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
- package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
- 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 +71 -85
- package/src/Extensions/LinkExtension/common.ts +10 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +41 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +6 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useExpandedSelection.ts +44 -0
- package/src/index.ts +5 -2
- package/src/types.ts +5 -0
- package/src/ui/Button/Button.tsx +10 -6
- package/src/ui/Button/_button.scss +1 -1
- package/src/ui/Fields/Input/Input.spec.tsx +7 -1
- package/src/ui/Fields/Input/Input.tsx +23 -4
- package/src/ui/Modal/Modal.spec.tsx +15 -0
- package/src/ui/Modal/Modal.tsx +8 -4
- package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
- package/src/ui/_forms.scss +14 -0
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +271 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +212 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +341 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +159 -0
- package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
- package/src/utils/resolveMatrixAssetUrl.ts +7 -0
- 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 +28 -15
- package/tsconfig.json +1 -1
- package/lib/FormattedTextEditor.d.ts +0 -2
- package/lib/FormattedTextEditor.js +0 -7
- package/src/Editor/Editor.mock.tsx +0 -43
- package/src/FormattedTextEditor.spec.tsx +0 -10
- package/src/FormattedTextEditor.tsx +0 -3
- /package/tests/{select.tsx → select.ts} +0 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.remirrorNodeToSquizNode = exports.resolveNodeTag = void 0;
|
4
|
+
const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
|
5
|
+
const Extensions_1 = require("../../../Extensions/Extensions");
|
6
|
+
const resolveNodeTag = (node) => {
|
7
|
+
if (node.type.name === 'text') {
|
8
|
+
return 'span';
|
9
|
+
}
|
10
|
+
if (node.type.spec?.toDOM) {
|
11
|
+
const domNode = node.type.spec.toDOM(node);
|
12
|
+
if (domNode instanceof window.Node) {
|
13
|
+
return domNode.nodeName.toLowerCase();
|
14
|
+
}
|
15
|
+
if (typeof domNode === 'object' && 'dom' in domNode && domNode.dom instanceof window.Node) {
|
16
|
+
return domNode.dom.nodeName.toLowerCase();
|
17
|
+
}
|
18
|
+
if (domNode instanceof Array) {
|
19
|
+
// [ tag, attributes, ...children ]
|
20
|
+
return domNode[0].toLowerCase();
|
21
|
+
}
|
22
|
+
}
|
23
|
+
throw new Error('Unexpected Remirror node encountered, cannot resolve tag.');
|
24
|
+
};
|
25
|
+
exports.resolveNodeTag = resolveNodeTag;
|
26
|
+
const resolveFormattingOptions = (node) => {
|
27
|
+
const formattingOptions = {};
|
28
|
+
if (node.attrs.nodeTextAlignment) {
|
29
|
+
formattingOptions.alignment = node.attrs.nodeTextAlignment;
|
30
|
+
}
|
31
|
+
return formattingOptions;
|
32
|
+
};
|
33
|
+
const resolveFontOptions = (node) => {
|
34
|
+
const fontOptions = {};
|
35
|
+
node.marks.forEach((mark) => {
|
36
|
+
switch (mark.type.name) {
|
37
|
+
case 'bold':
|
38
|
+
fontOptions.bold = true;
|
39
|
+
break;
|
40
|
+
case 'italic':
|
41
|
+
fontOptions.italics = true;
|
42
|
+
break;
|
43
|
+
case 'underline':
|
44
|
+
fontOptions.underline = true;
|
45
|
+
break;
|
46
|
+
}
|
47
|
+
});
|
48
|
+
return fontOptions;
|
49
|
+
};
|
50
|
+
const transformAttributes = (attributes) => {
|
51
|
+
const transformed = {};
|
52
|
+
Object.keys(attributes).forEach((key) => {
|
53
|
+
// Component service requires attributes to be a string, cast as needed.
|
54
|
+
if (typeof attributes[key] === 'string' || typeof attributes[key] === 'number') {
|
55
|
+
transformed[key] = String(attributes[key]);
|
56
|
+
}
|
57
|
+
});
|
58
|
+
return transformed;
|
59
|
+
};
|
60
|
+
const transformFragment = (fragment) => {
|
61
|
+
const transformed = [];
|
62
|
+
fragment.forEach((child) => transformed.push(transformNode(child)));
|
63
|
+
return transformed;
|
64
|
+
};
|
65
|
+
const transformNode = (node) => {
|
66
|
+
const attributes = node.type.name === Extensions_1.NodeName.Image ? transformAttributes(node.attrs) : undefined;
|
67
|
+
const formattingOptions = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFormattingOptions(node));
|
68
|
+
const font = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFontOptions(node));
|
69
|
+
let transformedNode = { type: 'text', value: node.text || '' };
|
70
|
+
// Squiz "text" nodes can't have formatting/font options but Remirror "text" nodes can.
|
71
|
+
// If the node has formatting options wrap in a tag.
|
72
|
+
// If the node isn't a text type assume it is a tag type and wrap in a tag.
|
73
|
+
// If we pick the wrong tag here it will be corrected later as part of looping through the
|
74
|
+
// non-font marks.
|
75
|
+
if (node.type.name !== Extensions_1.NodeName.Text || attributes || formattingOptions || font) {
|
76
|
+
transformedNode = {
|
77
|
+
type: 'tag',
|
78
|
+
tag: (0, exports.resolveNodeTag)(node),
|
79
|
+
children: node.type.name === Extensions_1.NodeName.Text ? [transformedNode] : transformFragment(node.content),
|
80
|
+
attributes,
|
81
|
+
formattingOptions,
|
82
|
+
font,
|
83
|
+
};
|
84
|
+
}
|
85
|
+
if (node.type.name === Extensions_1.NodeName.AssetImage) {
|
86
|
+
transformedNode = {
|
87
|
+
type: 'matrix-image',
|
88
|
+
matrixAssetId: node.attrs.matrixAssetId,
|
89
|
+
matrixIdentifier: node.attrs.matrixIdentifier,
|
90
|
+
matrixDomain: node.attrs.matrixDomain,
|
91
|
+
};
|
92
|
+
}
|
93
|
+
node.marks.forEach((mark) => {
|
94
|
+
switch (mark.type.name) {
|
95
|
+
case 'bold':
|
96
|
+
case 'italic':
|
97
|
+
case 'underline':
|
98
|
+
break;
|
99
|
+
default:
|
100
|
+
transformedNode = transformMark(mark, transformedNode);
|
101
|
+
}
|
102
|
+
});
|
103
|
+
return transformedNode;
|
104
|
+
};
|
105
|
+
/**
|
106
|
+
* Merges 2 nodes together if they are compatible without losing any important details.
|
107
|
+
* Otherwise will wrap the node.
|
108
|
+
*
|
109
|
+
* @param {FormattedNode} node
|
110
|
+
* @param {FormattedNodeWithChildren} wrappingNode
|
111
|
+
*
|
112
|
+
* @return {FormattedNode}
|
113
|
+
*/
|
114
|
+
const wrapNodeIfNeeded = (node, wrappingNode) => {
|
115
|
+
if (node.type === 'tag' && wrappingNode.type === 'tag' && (node.tag === 'span' || node.tag === wrappingNode.tag)) {
|
116
|
+
// if the node we are wrapping with is a DOM node, and the node being wrapped is
|
117
|
+
// a plain looking DOM node merge the 2 nodes.
|
118
|
+
return {
|
119
|
+
...node,
|
120
|
+
...wrappingNode,
|
121
|
+
formattingOptions: (0, undefinedIfEmpty_1.undefinedIfEmpty)({
|
122
|
+
...node.formattingOptions,
|
123
|
+
...wrappingNode.formattingOptions,
|
124
|
+
}),
|
125
|
+
attributes: (0, undefinedIfEmpty_1.undefinedIfEmpty)({
|
126
|
+
...node.attributes,
|
127
|
+
...wrappingNode.attributes,
|
128
|
+
}),
|
129
|
+
font: (0, undefinedIfEmpty_1.undefinedIfEmpty)({
|
130
|
+
...node.font,
|
131
|
+
...wrappingNode.font,
|
132
|
+
}),
|
133
|
+
children: [...node.children, ...wrappingNode.children],
|
134
|
+
};
|
135
|
+
}
|
136
|
+
// if the node we are wrapping or the wrapping nodes are not compatible merge them.
|
137
|
+
return {
|
138
|
+
...wrappingNode,
|
139
|
+
children: [node, ...wrappingNode.children],
|
140
|
+
};
|
141
|
+
};
|
142
|
+
const transformMark = (mark, node) => {
|
143
|
+
switch (mark.type.name) {
|
144
|
+
case 'link':
|
145
|
+
return wrapNodeIfNeeded(node, {
|
146
|
+
type: 'tag',
|
147
|
+
tag: 'a',
|
148
|
+
attributes: transformAttributes(mark.attrs),
|
149
|
+
children: [],
|
150
|
+
});
|
151
|
+
case 'assetLink':
|
152
|
+
return wrapNodeIfNeeded(node, {
|
153
|
+
type: 'link-to-matrix-asset',
|
154
|
+
target: mark.attrs.target,
|
155
|
+
matrixIdentifier: mark.attrs.matrixIdentifier,
|
156
|
+
matrixDomain: mark.attrs.matrixDomain,
|
157
|
+
matrixAssetId: mark.attrs.matrixAssetId,
|
158
|
+
children: [],
|
159
|
+
});
|
160
|
+
}
|
161
|
+
throw new Error(`Unsupported mark "${mark.type.name}" was applied to node.`);
|
162
|
+
};
|
163
|
+
/**
|
164
|
+
* Converts Remirror node JSON structure to Squiz component JSON structure.
|
165
|
+
* @param {ProsemirrorNode} node Remirror node to convert to component.
|
166
|
+
* @returns {FormattedText} The converted Squiz component JSON.
|
167
|
+
*/
|
168
|
+
const remirrorNodeToSquizNode = (node) => {
|
169
|
+
if (node?.type?.name !== 'doc') {
|
170
|
+
throw new Error('Unable to convert from Remirror to Node data structure, unexpected node provided.');
|
171
|
+
}
|
172
|
+
return transformFragment(node.content);
|
173
|
+
};
|
174
|
+
exports.remirrorNodeToSquizNode = remirrorNodeToSquizNode;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import { RemirrorJSON } from '@remirror/core';
|
2
|
+
import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
|
3
|
+
/**
|
4
|
+
* Converts Squiz component JSON structure to Remirror node JSON structure.
|
5
|
+
* @param {FormattedText} nodes Squiz nodes to convert to Remirror.
|
6
|
+
* @export
|
7
|
+
* @returns {RemirrorJSON} The converted Remirror JSON.
|
8
|
+
*/
|
9
|
+
export declare const squizNodeToRemirrorNode: (nodes: FormattedTextModels.v1.FormattedText) => RemirrorJSON;
|
@@ -0,0 +1,138 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.squizNodeToRemirrorNode = void 0;
|
4
|
+
const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
|
5
|
+
const Extensions_1 = require("../../../Extensions/Extensions");
|
6
|
+
const getNodeType = (node) => {
|
7
|
+
const typeMap = {
|
8
|
+
'link-to-matrix-asset': Extensions_1.NodeName.Text,
|
9
|
+
'matrix-image': Extensions_1.NodeName.AssetImage,
|
10
|
+
text: 'text',
|
11
|
+
};
|
12
|
+
const tagMap = {
|
13
|
+
h1: 'heading',
|
14
|
+
h2: 'heading',
|
15
|
+
h3: 'heading',
|
16
|
+
h4: 'heading',
|
17
|
+
h5: 'heading',
|
18
|
+
h6: 'heading',
|
19
|
+
img: 'image',
|
20
|
+
pre: 'preformatted',
|
21
|
+
p: 'paragraph',
|
22
|
+
a: Extensions_1.NodeName.Text,
|
23
|
+
span: Extensions_1.NodeName.Text,
|
24
|
+
};
|
25
|
+
if (typeMap[node.type]) {
|
26
|
+
return typeMap[node.type];
|
27
|
+
}
|
28
|
+
if (node.type === 'tag' && tagMap[node.tag]) {
|
29
|
+
return tagMap[node.tag];
|
30
|
+
}
|
31
|
+
// Unsupported node type
|
32
|
+
throw new Error(node.type === 'tag'
|
33
|
+
? `Unsupported node type provided: ${node.type} (tag: ${node.tag})`
|
34
|
+
: `Unsupported node type provided: ${node.type}`);
|
35
|
+
};
|
36
|
+
const getNodeAttributes = (node) => {
|
37
|
+
if (node.type === 'tag' && node.tag === 'img') {
|
38
|
+
return {
|
39
|
+
alt: node.attributes?.alt,
|
40
|
+
height: node.attributes?.height,
|
41
|
+
width: node.attributes?.width,
|
42
|
+
src: node.attributes?.src,
|
43
|
+
title: node.attributes?.title,
|
44
|
+
};
|
45
|
+
}
|
46
|
+
else if (node.type === 'matrix-image') {
|
47
|
+
return {
|
48
|
+
matrixAssetId: node.matrixAssetId,
|
49
|
+
matrixDomain: node.matrixDomain,
|
50
|
+
matrixIdentifier: node.matrixIdentifier,
|
51
|
+
};
|
52
|
+
}
|
53
|
+
else if (node.type === 'tag') {
|
54
|
+
return {
|
55
|
+
nodeIndent: null,
|
56
|
+
nodeTextAlignment: node.formattingOptions?.alignment || null,
|
57
|
+
nodeLineHeight: null,
|
58
|
+
style: '',
|
59
|
+
level: node.tag?.startsWith('h') ? parseInt(node.tag.substring(1)) : undefined,
|
60
|
+
};
|
61
|
+
}
|
62
|
+
return {};
|
63
|
+
};
|
64
|
+
const getNodeMarks = (node) => {
|
65
|
+
const marks = [];
|
66
|
+
if (node.type === 'tag' && node.tag === 'a') {
|
67
|
+
marks.push({
|
68
|
+
type: Extensions_1.MarkName.Link,
|
69
|
+
attrs: {
|
70
|
+
href: node.attributes?.href,
|
71
|
+
target: node.attributes?.target ?? null,
|
72
|
+
auto: false,
|
73
|
+
title: node.attributes?.title ?? null,
|
74
|
+
},
|
75
|
+
});
|
76
|
+
}
|
77
|
+
else if (node.type === 'link-to-matrix-asset') {
|
78
|
+
marks.push({
|
79
|
+
type: Extensions_1.MarkName.AssetLink,
|
80
|
+
attrs: {
|
81
|
+
matrixAssetId: node.matrixAssetId,
|
82
|
+
matrixDomain: node.matrixDomain,
|
83
|
+
matrixIdentifier: node.matrixIdentifier,
|
84
|
+
target: node.target,
|
85
|
+
},
|
86
|
+
});
|
87
|
+
}
|
88
|
+
// Handle font formatting
|
89
|
+
if ('font' in node) {
|
90
|
+
node.font?.bold && marks.push({ type: 'bold' });
|
91
|
+
node.font?.italics && marks.push({ type: 'italic' });
|
92
|
+
node.font?.underline && marks.push({ type: 'underline' });
|
93
|
+
}
|
94
|
+
return marks;
|
95
|
+
};
|
96
|
+
const unwrapNodeIfNeeded = (node) => {
|
97
|
+
if (node.type === 'text' && node.content?.length) {
|
98
|
+
return node.content.map((child) => {
|
99
|
+
return {
|
100
|
+
...child,
|
101
|
+
marks: [...(child.marks || []), ...(node.marks || [])],
|
102
|
+
};
|
103
|
+
});
|
104
|
+
}
|
105
|
+
return [node];
|
106
|
+
};
|
107
|
+
const formatNode = (node) => {
|
108
|
+
const children = [];
|
109
|
+
if ('children' in node) {
|
110
|
+
node.children.forEach((child) => {
|
111
|
+
children.push(...formatNode(child));
|
112
|
+
});
|
113
|
+
}
|
114
|
+
return unwrapNodeIfNeeded({
|
115
|
+
type: getNodeType(node),
|
116
|
+
attrs: (0, undefinedIfEmpty_1.undefinedIfEmpty)(getNodeAttributes(node)),
|
117
|
+
marks: (0, undefinedIfEmpty_1.undefinedIfEmpty)(getNodeMarks(node)),
|
118
|
+
text: node.type === 'text' ? node.value : undefined,
|
119
|
+
content: (0, undefinedIfEmpty_1.undefinedIfEmpty)(children),
|
120
|
+
});
|
121
|
+
};
|
122
|
+
/**
|
123
|
+
* Converts Squiz component JSON structure to Remirror node JSON structure.
|
124
|
+
* @param {FormattedText} nodes Squiz nodes to convert to Remirror.
|
125
|
+
* @export
|
126
|
+
* @returns {RemirrorJSON} The converted Remirror JSON.
|
127
|
+
*/
|
128
|
+
const squizNodeToRemirrorNode = (nodes) => {
|
129
|
+
let children = [];
|
130
|
+
nodes.forEach((node) => {
|
131
|
+
children.push(...formatNode(node));
|
132
|
+
});
|
133
|
+
if (children.find((child) => child.type === 'text')) {
|
134
|
+
children = [{ type: 'paragraph', content: children }];
|
135
|
+
}
|
136
|
+
return { type: 'doc', content: children };
|
137
|
+
};
|
138
|
+
exports.squizNodeToRemirrorNode = squizNodeToRemirrorNode;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const resolveMatrixAssetUrl: (id: string, matrixDomain: string) => string;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.resolveMatrixAssetUrl = void 0;
|
4
|
+
const resolveMatrixAssetUrl = (id, matrixDomain) => {
|
5
|
+
if (matrixDomain.indexOf('://') < 0) {
|
6
|
+
matrixDomain = `${window.location.protocol}//${matrixDomain}`;
|
7
|
+
}
|
8
|
+
return new URL(`/_nocache?a=${encodeURIComponent(id)}`, matrixDomain).toString();
|
9
|
+
};
|
10
|
+
exports.resolveMatrixAssetUrl = resolveMatrixAssetUrl;
|
@@ -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.
|
3
|
+
"version": "1.22.0",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"scripts": {
|
@@ -20,20 +20,24 @@
|
|
20
20
|
"@headlessui/react": "1.7.11",
|
21
21
|
"@mui/icons-material": "5.11.0",
|
22
22
|
"@remirror/react": "2.0.25",
|
23
|
+
"@squiz/dx-json-schema-lib": "1.21.1-alpha.2",
|
23
24
|
"clsx": "1.2.1",
|
24
25
|
"react-hook-form": "7.43.2",
|
25
|
-
"react-image-size": "2.0.0"
|
26
|
+
"react-image-size": "2.0.0",
|
27
|
+
"remirror": "2.0.26"
|
26
28
|
},
|
27
29
|
"devDependencies": {
|
28
30
|
"@testing-library/cypress": "9.0.0",
|
29
31
|
"@testing-library/jest-dom": "5.16.5",
|
30
32
|
"@testing-library/react": "14.0.0",
|
31
33
|
"@testing-library/user-event": "14.4.3",
|
34
|
+
"@types/node": "18.15.2",
|
32
35
|
"@types/react": "18.0.26",
|
33
36
|
"@types/react-dom": "18.0.9",
|
34
37
|
"@vitejs/plugin-react": "3.0.0",
|
35
38
|
"autoprefixer": "10.4.13",
|
36
39
|
"cypress": "12.5.1",
|
40
|
+
"deepmerge": "4.3.0",
|
37
41
|
"eslint-plugin-cypress": "2.12.1",
|
38
42
|
"eslint-plugin-jsx-a11y": "6.7.1",
|
39
43
|
"eslint-plugin-react": "7.32.2",
|
@@ -44,6 +48,7 @@
|
|
44
48
|
"postcss-nested": "6.0.0",
|
45
49
|
"postcss-prefix-selector": "1.16.0",
|
46
50
|
"react": "18.2.0",
|
51
|
+
"react-diff-viewer-continued": "3.2.6",
|
47
52
|
"react-dom": "18.2.0",
|
48
53
|
"rimraf": "4.1.2",
|
49
54
|
"tailwindcss": "3.2.6",
|
@@ -54,6 +59,7 @@
|
|
54
59
|
"peerDependencies": {
|
55
60
|
"@types/react": "^16.14.0 || ^17 || ^18",
|
56
61
|
"@types/react-dom": "^16.9.0 || ^17 || ^18",
|
62
|
+
"lib0": "0.2.69",
|
57
63
|
"react": "^16.14.0 || ^17 || ^18",
|
58
64
|
"react-dom": "^16.14.0 || ^17 || ^18"
|
59
65
|
},
|
@@ -66,7 +72,7 @@
|
|
66
72
|
}
|
67
73
|
},
|
68
74
|
"volta": {
|
69
|
-
"node": "
|
75
|
+
"node": "18.15.0"
|
70
76
|
},
|
71
|
-
"gitHead": "
|
77
|
+
"gitHead": "c683f01b30c8b35cc23c5bd7426778a94467baec"
|
72
78
|
}
|
@@ -1,11 +1,9 @@
|
|
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
|
-
|
8
|
-
|
5
|
+
import { renderWithEditor } from '../../tests/renderWithEditor';
|
6
|
+
import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
|
9
7
|
|
10
8
|
describe('Formatted text editor', () => {
|
11
9
|
it('Renders the text editor', () => {
|
@@ -230,25 +228,87 @@ describe('Formatted text editor', () => {
|
|
230
228
|
expect(baseElement.querySelectorAll('div.remirror-editor p')).toHaveLength(1);
|
231
229
|
});
|
232
230
|
|
233
|
-
it('
|
234
|
-
const {
|
231
|
+
it('should allow text to be pasted into the editor', async () => {
|
232
|
+
const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
233
|
+
content: 'Some nonsense content here',
|
234
|
+
});
|
235
|
+
|
236
|
+
// paste something
|
237
|
+
await act(() => editor.paste('I pasted this! '));
|
238
|
+
|
239
|
+
expect(getJsonContent()).toEqual({
|
240
|
+
type: 'paragraph',
|
241
|
+
attrs: expect.any(Object),
|
242
|
+
content: [
|
243
|
+
{
|
244
|
+
text: 'I pasted this! Some nonsense content here',
|
245
|
+
type: 'text',
|
246
|
+
},
|
247
|
+
],
|
248
|
+
});
|
249
|
+
});
|
250
|
+
|
251
|
+
it('should not allow images to be pasted into the editor', async () => {
|
252
|
+
const { elements, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
253
|
+
content: 'Some nonsense content here',
|
254
|
+
});
|
255
|
+
const imageClipboardData = {
|
256
|
+
dropEffect: 'none',
|
257
|
+
effectAllowed: 'uninitialized',
|
258
|
+
items: {
|
259
|
+
'0': {
|
260
|
+
kind: 'file',
|
261
|
+
type: 'image/png',
|
262
|
+
},
|
263
|
+
},
|
264
|
+
files: {
|
265
|
+
'0': {
|
266
|
+
lastModified: 1678233857792,
|
267
|
+
lastModifiedDate: {},
|
268
|
+
name: 'Screen Shot 2023-03-08 at 10.04.12 am.png',
|
269
|
+
size: 58517,
|
270
|
+
type: 'image/png',
|
271
|
+
webkitRelativePath: '',
|
272
|
+
},
|
273
|
+
},
|
274
|
+
types: [],
|
275
|
+
};
|
276
|
+
|
277
|
+
// paste something
|
278
|
+
await act(() => fireEvent.paste(elements.editor, imageClipboardData));
|
279
|
+
|
280
|
+
expect(getJsonContent()).toEqual({
|
281
|
+
type: 'paragraph',
|
282
|
+
attrs: expect.any(Object),
|
283
|
+
content: [
|
284
|
+
{
|
285
|
+
text: 'Some nonsense content here',
|
286
|
+
type: 'text',
|
287
|
+
},
|
288
|
+
],
|
289
|
+
});
|
290
|
+
});
|
235
291
|
|
236
|
-
|
292
|
+
it('Should not display the toolbar if is not editable', () => {
|
293
|
+
render(<Editor editable={false} />);
|
294
|
+
expect(screen.queryByRole('button', { name: 'Bold (cmd+B)' })).not.toBeInTheDocument();
|
295
|
+
expect(screen.queryByRole('button', { name: 'Italic (cmd+I)' })).not.toBeInTheDocument();
|
296
|
+
expect(screen.queryByRole('button', { name: 'Underline (cmd+U)' })).not.toBeInTheDocument();
|
297
|
+
});
|
237
298
|
|
238
|
-
|
239
|
-
|
240
|
-
|
299
|
+
it('Should not display the floating toolbar if is not editable', async () => {
|
300
|
+
const from = 3 as number;
|
301
|
+
const to = 17 as number;
|
302
|
+
const { editor } = await renderWithEditor(null, {
|
303
|
+
content: 'My awesome <a href="https://example.org">example</a> content.',
|
304
|
+
editable: false,
|
241
305
|
});
|
242
306
|
|
243
|
-
|
244
|
-
expect(editorNode).toBeTruthy();
|
245
|
-
expect(editorNode.textContent).toBe(textContent);
|
307
|
+
await act(() => editor.selectText({ from, to }));
|
246
308
|
|
247
|
-
|
248
|
-
const
|
249
|
-
expect(undoButton).toBeTruthy();
|
250
|
-
fireEvent.click(undoButton);
|
309
|
+
const buttons = screen.queryAllByRole('button');
|
310
|
+
const buttonLabels = buttons.map((button) => button.getAttribute('title'));
|
251
311
|
|
252
|
-
expect(
|
312
|
+
expect(buttonLabels).toEqual([]);
|
253
313
|
});
|
254
314
|
});
|
package/src/Editor/Editor.tsx
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
|
1
|
+
import React, { useContext, useCallback } from 'react';
|
2
|
+
import { EditorComponent, Remirror, useRemirror, useEditorEvent } 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';
|
7
|
+
import clsx from 'clsx';
|
8
|
+
import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
|
6
9
|
|
7
10
|
type EditorProps = {
|
8
11
|
content?: RemirrorContentType;
|
@@ -10,9 +13,25 @@ type EditorProps = {
|
|
10
13
|
editable?: boolean;
|
11
14
|
};
|
12
15
|
|
13
|
-
const
|
16
|
+
const WrappedEditor = () => {
|
17
|
+
const preventImagePaste = useCallback((event) => {
|
18
|
+
const { clipboardData } = event;
|
19
|
+
const pastedData = clipboardData?.files[0];
|
20
|
+
if (pastedData?.type && pastedData?.type.startsWith('image/')) {
|
21
|
+
event.preventDefault();
|
22
|
+
}
|
23
|
+
|
24
|
+
// Allow other paste event handlers to be run.
|
25
|
+
return false;
|
26
|
+
}, []) as ClipboardEventHandler;
|
27
|
+
|
28
|
+
useEditorEvent('paste', preventImagePaste);
|
29
|
+
return <EditorComponent />;
|
30
|
+
};
|
31
|
+
|
32
|
+
const Editor = ({ content, editable = true, onChange }: EditorProps) => {
|
14
33
|
const { manager, state, setState } = useRemirror({
|
15
|
-
extensions:
|
34
|
+
extensions: createExtensions(useContext(EditorContext)),
|
16
35
|
content,
|
17
36
|
selection: 'start',
|
18
37
|
stringHandler: 'html',
|
@@ -25,7 +44,7 @@ const Editor = ({ content, editable, onChange }: EditorProps) => {
|
|
25
44
|
|
26
45
|
return (
|
27
46
|
<div className="squiz-fte-scope">
|
28
|
-
<div className=
|
47
|
+
<div className={clsx('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled')}>
|
29
48
|
<Remirror
|
30
49
|
manager={manager}
|
31
50
|
state={state}
|
@@ -34,9 +53,9 @@ const Editor = ({ content, editable, onChange }: EditorProps) => {
|
|
34
53
|
placeholder="Write something"
|
35
54
|
label="Text editor"
|
36
55
|
>
|
37
|
-
<Toolbar />
|
38
|
-
<
|
39
|
-
<FloatingToolbar />
|
56
|
+
{editable && <Toolbar />}
|
57
|
+
<WrappedEditor />
|
58
|
+
{editable && <FloatingToolbar />}
|
40
59
|
</Remirror>
|
41
60
|
</div>
|
42
61
|
</div>
|
@@ -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', async () => {
|
13
|
+
render(<Component />);
|
14
|
+
|
15
|
+
const defaultContext = defaultContextFn.mock.calls[0][0];
|
16
|
+
|
17
|
+
expect(defaultContext).toEqual({
|
18
|
+
matrix: {
|
19
|
+
resolveMatrixAsset: expect.any(Function),
|
20
|
+
matrixDomain: '',
|
21
|
+
matrixIdentifier: '',
|
22
|
+
},
|
23
|
+
});
|
24
|
+
expect(await defaultContext.matrix.resolveMatrixAsset('fake-asset-id')).toBeNull();
|
25
|
+
});
|
26
|
+
});
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
export type MatrixAsset = {
|
4
|
+
id: string;
|
5
|
+
type: string | 'image';
|
6
|
+
};
|
7
|
+
|
8
|
+
export type MatrixAssetResolver = (assetId: string) => Promise<MatrixAsset | null>;
|
9
|
+
|
10
|
+
export type EditorContextOptions = {
|
11
|
+
matrix: {
|
12
|
+
matrixIdentifier: string;
|
13
|
+
matrixDomain: string;
|
14
|
+
resolveMatrixAsset: MatrixAssetResolver;
|
15
|
+
};
|
16
|
+
};
|
17
|
+
|
18
|
+
export const defaultEditorContext: EditorContextOptions = {
|
19
|
+
matrix: {
|
20
|
+
matrixIdentifier: '',
|
21
|
+
matrixDomain: '',
|
22
|
+
resolveMatrixAsset: () => Promise.resolve(null),
|
23
|
+
},
|
24
|
+
};
|
25
|
+
|
26
|
+
export const EditorContext = React.createContext(defaultEditorContext);
|