@squiz/formatted-text-editor 1.21.1-alpha.9 → 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 (123) hide show
  1. package/demo/App.tsx +38 -10
  2. package/demo/index.scss +2 -7
  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 +11 -1
  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.js +6 -2
  33. package/lib/hooks/index.d.ts +1 -0
  34. package/lib/hooks/index.js +1 -0
  35. package/lib/hooks/useExpandedSelection.d.ts +23 -0
  36. package/lib/hooks/useExpandedSelection.js +37 -0
  37. package/lib/index.css +58 -26
  38. package/lib/index.d.ts +3 -2
  39. package/lib/index.js +5 -3
  40. package/lib/types.d.ts +3 -0
  41. package/lib/types.js +2 -0
  42. package/lib/ui/Button/Button.d.ts +2 -1
  43. package/lib/ui/Button/Button.js +4 -5
  44. package/lib/ui/Fields/Input/Input.d.ts +1 -0
  45. package/lib/ui/Fields/Input/Input.js +9 -3
  46. package/lib/ui/Modal/Modal.js +5 -3
  47. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +1 -2
  48. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +118 -104
  49. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +102 -69
  50. package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
  51. package/lib/utils/resolveMatrixAssetUrl.js +10 -0
  52. package/lib/utils/undefinedIfEmpty.d.ts +1 -0
  53. package/lib/utils/undefinedIfEmpty.js +7 -0
  54. package/package.json +8 -4
  55. package/src/Editor/Editor.spec.tsx +78 -18
  56. package/src/Editor/Editor.tsx +28 -9
  57. package/src/Editor/EditorContext.spec.tsx +26 -0
  58. package/src/Editor/EditorContext.ts +26 -0
  59. package/src/Editor/_editor.scss +20 -4
  60. package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
  61. package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
  62. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
  63. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
  64. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +250 -2
  65. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
  66. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +59 -20
  67. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +12 -10
  68. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
  69. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
  70. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +137 -26
  71. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
  72. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
  73. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
  74. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
  75. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
  76. package/src/EditorToolbar/_floating-toolbar.scss +4 -5
  77. package/src/EditorToolbar/_toolbar.scss +1 -1
  78. package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
  79. package/src/Extensions/Extensions.ts +42 -19
  80. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
  81. package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
  82. package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
  83. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
  84. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
  85. package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
  86. package/src/Extensions/LinkExtension/LinkExtension.ts +71 -85
  87. package/src/Extensions/LinkExtension/common.ts +10 -0
  88. package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +41 -0
  89. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +6 -2
  90. package/src/hooks/index.ts +1 -0
  91. package/src/hooks/useExpandedSelection.ts +44 -0
  92. package/src/index.ts +3 -2
  93. package/src/types.ts +5 -0
  94. package/src/ui/Button/Button.tsx +10 -6
  95. package/src/ui/Button/_button.scss +1 -1
  96. package/src/ui/Fields/Input/Input.spec.tsx +7 -1
  97. package/src/ui/Fields/Input/Input.tsx +23 -4
  98. package/src/ui/Modal/Modal.spec.tsx +15 -0
  99. package/src/ui/Modal/Modal.tsx +8 -4
  100. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
  101. package/src/ui/_forms.scss +14 -0
  102. package/src/utils/converters/mocks/squizNodeJson.mock.ts +196 -0
  103. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +41 -6
  104. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +132 -111
  105. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +68 -34
  106. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +115 -79
  107. package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
  108. package/src/utils/resolveMatrixAssetUrl.ts +7 -0
  109. package/src/utils/undefinedIfEmpty.spec.ts +12 -0
  110. package/src/utils/undefinedIfEmpty.ts +3 -0
  111. package/tailwind.config.cjs +3 -0
  112. package/tests/renderWithEditor.tsx +26 -13
  113. package/tsconfig.json +1 -1
  114. package/lib/FormattedTextEditor.d.ts +0 -2
  115. package/lib/FormattedTextEditor.js +0 -7
  116. package/lib/utils/converters/validNodeTypes.d.ts +0 -2
  117. package/lib/utils/converters/validNodeTypes.js +0 -21
  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/src/utils/converters/validNodeTypes.spec.ts +0 -33
  122. package/src/utils/converters/validNodeTypes.ts +0 -21
  123. /package/tests/{select.tsx → select.ts} +0 -0
@@ -1,14 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.remirrorNodeToSquizNode = exports.resolveNodeTag = void 0;
4
- const validNodeTypes_1 = require("../validNodeTypes");
4
+ const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
5
+ const Extensions_1 = require("../../../Extensions/Extensions");
5
6
  const resolveNodeTag = (node) => {
7
+ if (node.type.name === 'text') {
8
+ return 'span';
9
+ }
6
10
  if (node.type.spec?.toDOM) {
7
11
  const domNode = node.type.spec.toDOM(node);
8
12
  if (domNode instanceof window.Node) {
9
13
  return domNode.nodeName.toLowerCase();
10
14
  }
11
- if (domNode?.dom instanceof window.Node) {
15
+ if (typeof domNode === 'object' && 'dom' in domNode && domNode.dom instanceof window.Node) {
12
16
  return domNode.dom.nodeName.toLowerCase();
13
17
  }
14
18
  if (domNode instanceof Array) {
@@ -16,7 +20,7 @@ const resolveNodeTag = (node) => {
16
20
  return domNode[0].toLowerCase();
17
21
  }
18
22
  }
19
- return null;
23
+ throw new Error('Unexpected Remirror node encountered, cannot resolve tag.');
20
24
  };
21
25
  exports.resolveNodeTag = resolveNodeTag;
22
26
  const resolveFormattingOptions = (node) => {
@@ -39,122 +43,132 @@ const resolveFontOptions = (node) => {
39
43
  case 'underline':
40
44
  fontOptions.underline = true;
41
45
  break;
42
- default:
43
- // Currently unsupported mark type
44
- break;
45
46
  }
46
47
  });
47
48
  return fontOptions;
48
49
  };
49
- const resolveAttributeOptions = (node, nodeType) => {
50
- let attributeOptions = {};
51
- if (nodeType === 'image') {
52
- attributeOptions = { ...node.attrs };
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
+ };
53
84
  }
54
- else {
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
- });
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
+ };
65
92
  }
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
71
- }
72
- else {
73
- attributeOptions[key] = String(attributeOptions[key]);
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);
74
101
  }
75
102
  });
76
- return attributeOptions;
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.`);
77
162
  };
78
163
  /**
79
164
  * Converts Remirror node JSON structure to Squiz component JSON structure.
80
165
  * @param {ProsemirrorNode} node Remirror node to convert to component.
81
- * @export
82
166
  * @returns {FormattedText} The converted Squiz component JSON.
83
167
  */
84
168
  const remirrorNodeToSquizNode = (node) => {
85
- if (!(0, validNodeTypes_1.validRemirrorNode)(node))
86
- return [];
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;
169
+ if (node?.type?.name !== 'doc') {
170
+ throw new Error('Unable to convert from Remirror to Node data structure, unexpected node provided.');
153
171
  }
154
- // Remove empty attributes options from transformed object.
155
- if (transformed.attributes && Object.keys(transformed.attributes).length === 0) {
156
- delete transformed.attributes;
157
- }
158
- return transformed;
172
+ return transformFragment(node.content);
159
173
  };
160
174
  exports.remirrorNodeToSquizNode = remirrorNodeToSquizNode;
@@ -1,8 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.squizNodeToRemirrorNode = void 0;
4
+ const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
5
+ const Extensions_1 = require("../../../Extensions/Extensions");
4
6
  const getNodeType = (node) => {
5
- const nodeTypeMap = {
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 = {
6
13
  h1: 'heading',
7
14
  h2: 'heading',
8
15
  h3: 'heading',
@@ -12,83 +19,105 @@ const getNodeType = (node) => {
12
19
  img: 'image',
13
20
  pre: 'preformatted',
14
21
  p: 'paragraph',
15
- text: 'paragraph',
22
+ a: Extensions_1.NodeName.Text,
23
+ span: Extensions_1.NodeName.Text,
16
24
  };
17
- const nodeType = nodeTypeMap[node.tag || node.type];
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
+ }
18
31
  // Unsupported node type
19
- if (!nodeType)
20
- throw new Error(`Unsupported node type provided: ${node.tag}`);
21
- return nodeType;
32
+ throw new Error(node.type === 'tag'
33
+ ? `Unsupported node type provided: ${node.type} (tag: ${node.tag})`
34
+ : `Unsupported node type provided: ${node.type}`);
22
35
  };
23
36
  const getNodeAttributes = (node) => {
24
- const { alignment } = node.formattingOptions || {};
25
- return {
26
- nodeIndent: null,
27
- nodeTextAlignment: alignment ?? null,
28
- nodeLineHeight: null,
29
- style: '',
30
- level: node.tag?.startsWith('h') ? parseInt(node.tag.substring(1)) : undefined,
31
- };
32
- };
33
- const resolveChild = (child) => {
34
- if (child.type === 'text') {
35
- return { type: 'text', text: child.value };
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
+ };
36
45
  }
37
- let text = '';
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) => {
38
65
  const marks = [];
39
- if (child.type === 'tag') {
40
- // Handle link type
41
- if (child.tag === 'a') {
42
- marks.push({
43
- type: 'link',
44
- attrs: {
45
- href: child.attributes?.href,
46
- target: child.attributes?.target ?? null,
47
- auto: false,
48
- title: child.attributes?.title ?? null,
49
- },
50
- });
51
- }
52
- // Handle image type
53
- if (child.tag === 'img') {
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) => {
54
99
  return {
55
- type: 'image',
56
- attrs: {
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
- },
100
+ ...child,
101
+ marks: [...(child.marks || []), ...(node.marks || [])],
63
102
  };
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 : '';
103
+ });
71
104
  }
72
- return { type: 'text', marks, text };
105
+ return [node];
73
106
  };
74
107
  const formatNode = (node) => {
75
- let content;
76
- if (node.type === 'tag') {
77
- content = node.children.length ? node.children.map((child) => resolveChild(child)) : undefined;
108
+ const children = [];
109
+ if ('children' in node) {
110
+ node.children.forEach((child) => {
111
+ children.push(...formatNode(child));
112
+ });
78
113
  }
79
- if (node.type === 'text') {
80
- content = [
81
- {
82
- type: 'text',
83
- text: node.value,
84
- },
85
- ];
86
- }
87
- return {
114
+ return unwrapNodeIfNeeded({
88
115
  type: getNodeType(node),
89
- attrs: getNodeAttributes(node),
90
- content,
91
- };
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
+ });
92
121
  };
93
122
  /**
94
123
  * Converts Squiz component JSON structure to Remirror node JSON structure.
@@ -97,9 +126,13 @@ const formatNode = (node) => {
97
126
  * @returns {RemirrorJSON} The converted Remirror JSON.
98
127
  */
99
128
  const squizNodeToRemirrorNode = (nodes) => {
100
- return {
101
- type: 'doc',
102
- content: nodes.filter((node) => getNodeType(node)).map(formatNode),
103
- };
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 };
104
137
  };
105
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.21.1-alpha.9",
3
+ "version": "1.22.0",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -23,18 +23,21 @@
23
23
  "@squiz/dx-json-schema-lib": "1.21.1-alpha.2",
24
24
  "clsx": "1.2.1",
25
25
  "react-hook-form": "7.43.2",
26
- "react-image-size": "2.0.0"
26
+ "react-image-size": "2.0.0",
27
+ "remirror": "2.0.26"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@testing-library/cypress": "9.0.0",
30
31
  "@testing-library/jest-dom": "5.16.5",
31
32
  "@testing-library/react": "14.0.0",
32
33
  "@testing-library/user-event": "14.4.3",
34
+ "@types/node": "18.15.2",
33
35
  "@types/react": "18.0.26",
34
36
  "@types/react-dom": "18.0.9",
35
37
  "@vitejs/plugin-react": "3.0.0",
36
38
  "autoprefixer": "10.4.13",
37
39
  "cypress": "12.5.1",
40
+ "deepmerge": "4.3.0",
38
41
  "eslint-plugin-cypress": "2.12.1",
39
42
  "eslint-plugin-jsx-a11y": "6.7.1",
40
43
  "eslint-plugin-react": "7.32.2",
@@ -56,6 +59,7 @@
56
59
  "peerDependencies": {
57
60
  "@types/react": "^16.14.0 || ^17 || ^18",
58
61
  "@types/react-dom": "^16.9.0 || ^17 || ^18",
62
+ "lib0": "0.2.69",
59
63
  "react": "^16.14.0 || ^17 || ^18",
60
64
  "react-dom": "^16.14.0 || ^17 || ^18"
61
65
  },
@@ -68,7 +72,7 @@
68
72
  }
69
73
  },
70
74
  "volta": {
71
- "node": "16.19.0"
75
+ "node": "18.15.0"
72
76
  },
73
- "gitHead": "64afcee1b33efbff451aa0ef4e727c4e8a079621"
77
+ "gitHead": "c683f01b30c8b35cc23c5bd7426778a94467baec"
74
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
- const setContent: any = jest.fn();
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('Should allow text input & undo input upon clicking undo button', () => {
234
- const { baseElement, getByLabelText } = render(<MockEditor setContent={setContent} />);
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
- const textContent = `This is a string with a random number: ${Math.random() * 9999}`;
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
- // This sets the content of the text editor
239
- act(() => {
240
- setContent(`<p>${textContent}</p>`, { triggerChange: true });
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
- const editorNode = getByLabelText(`Text editor`);
244
- expect(editorNode).toBeTruthy();
245
- expect(editorNode.textContent).toBe(textContent);
307
+ await act(() => editor.selectText({ from, to }));
246
308
 
247
- // Testing if clicking undo button removes text from editor
248
- const undoButton = baseElement.querySelector('button[title="Undo (cmd+Z)"]') as HTMLButtonElement;
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(editorNode.textContent).toBe('');
312
+ expect(buttonLabels).toEqual([]);
253
313
  });
254
314
  });