@squiz/formatted-text-editor 1.35.0 → 1.35.1-alpha.1
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/lib/Extensions/Extensions.d.ts +2 -1
- package/lib/Extensions/Extensions.js +3 -0
- package/lib/Extensions/UnsuportedExtension/UnsupportedNodeExtension.d.ts +10 -0
- package/lib/Extensions/UnsuportedExtension/UnsupportedNodeExtension.js +76 -0
- package/lib/index.css +35 -0
- package/lib/ui/CollapseBox/CollapseBox.d.ts +7 -0
- package/lib/ui/CollapseBox/CollapseBox.js +48 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +4 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +29 -5
- package/package.json +4 -4
- package/src/Extensions/Extensions.ts +3 -0
- package/src/Extensions/UnsuportedExtension/UnsupportedNodeExtension.spec.ts +137 -0
- package/src/Extensions/UnsuportedExtension/UnsupportedNodeExtension.tsx +80 -0
- package/src/index.scss +1 -0
- package/src/ui/CollapseBox/CollapseBox.spec.tsx +49 -0
- package/src/ui/CollapseBox/CollapseBox.tsx +36 -0
- package/src/ui/CollapseBox/_collapseBox.scss +23 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +5 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +54 -4
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +27 -5
- package/src/utils/getNodeNamesByGroup.spec.ts +1 -1
@@ -10,12 +10,14 @@ const CommandsExtension_1 = require("./CommandsExtension/CommandsExtension");
|
|
10
10
|
const AssetImageExtension_1 = require("./ImageExtension/AssetImageExtension");
|
11
11
|
const CodeBlockExtension_1 = require("./CodeBlockExtension/CodeBlockExtension");
|
12
12
|
const ClearFormattingExtension_1 = require("./ClearFormattingExtension/ClearFormattingExtension");
|
13
|
+
const UnsupportedNodeExtension_1 = require("./UnsuportedExtension/UnsupportedNodeExtension");
|
13
14
|
var NodeName;
|
14
15
|
(function (NodeName) {
|
15
16
|
NodeName["Image"] = "image";
|
16
17
|
NodeName["CodeBlock"] = "codeBlock";
|
17
18
|
NodeName["AssetImage"] = "assetImage";
|
18
19
|
NodeName["Text"] = "text";
|
20
|
+
NodeName["Unsupported"] = "unsupportedNode";
|
19
21
|
})(NodeName = exports.NodeName || (exports.NodeName = {}));
|
20
22
|
var MarkName;
|
21
23
|
(function (MarkName) {
|
@@ -44,6 +46,7 @@ const createExtensions = (context) => {
|
|
44
46
|
new AssetLinkExtension_1.AssetLinkExtension({
|
45
47
|
matrixDomain: context.matrix.matrixDomain,
|
46
48
|
}),
|
49
|
+
new UnsupportedNodeExtension_1.UnsupportedNodeExtension(),
|
47
50
|
new ClearFormattingExtension_1.ClearFormattingExtension(),
|
48
51
|
];
|
49
52
|
};
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { ApplySchemaAttributes, NodeExtension, NodeExtensionSpec, NodeSpecOverride } from '@remirror/core';
|
2
|
+
import { NodeName } from '../Extensions';
|
3
|
+
import { ComponentType } from 'react';
|
4
|
+
import { NodeViewComponentProps } from '@remirror/react';
|
5
|
+
export declare class UnsupportedNodeExtension extends NodeExtension {
|
6
|
+
get name(): NodeName.Unsupported;
|
7
|
+
ReactComponent: ComponentType<NodeViewComponentProps>;
|
8
|
+
createTags(): "inline"[];
|
9
|
+
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec;
|
10
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
7
|
+
};
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
10
|
+
};
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
12
|
+
exports.UnsupportedNodeExtension = void 0;
|
13
|
+
const core_1 = require("@remirror/core");
|
14
|
+
const Extensions_1 = require("../Extensions");
|
15
|
+
const CollapseBox_1 = __importDefault(require("../../ui/CollapseBox/CollapseBox"));
|
16
|
+
let UnsupportedNodeExtension = class UnsupportedNodeExtension extends core_1.NodeExtension {
|
17
|
+
get name() {
|
18
|
+
return Extensions_1.NodeName.Unsupported;
|
19
|
+
}
|
20
|
+
ReactComponent = CollapseBox_1.default;
|
21
|
+
createTags() {
|
22
|
+
return [core_1.ExtensionTag.InlineNode];
|
23
|
+
}
|
24
|
+
createNodeSpec(extra, override) {
|
25
|
+
return {
|
26
|
+
selectable: false,
|
27
|
+
draggable: true,
|
28
|
+
atom: true,
|
29
|
+
inline: true,
|
30
|
+
...override,
|
31
|
+
attrs: {
|
32
|
+
...extra.defaults(),
|
33
|
+
originalNode: {},
|
34
|
+
errorMessage: {},
|
35
|
+
},
|
36
|
+
parseDOM: [
|
37
|
+
{
|
38
|
+
tag: `[data-unsupported-node]`,
|
39
|
+
getAttrs: (node) => {
|
40
|
+
if (!(0, core_1.isElementDomNode)(node)) {
|
41
|
+
return false;
|
42
|
+
}
|
43
|
+
const unsupportedNodes = node.getAttribute('data-unsupported-node');
|
44
|
+
try {
|
45
|
+
const decodedNodes = JSON.parse(unsupportedNodes);
|
46
|
+
return {
|
47
|
+
...extra.parse(node),
|
48
|
+
originalNode: decodedNodes?.originalNode,
|
49
|
+
errorMessage: decodedNodes?.errorMessage,
|
50
|
+
};
|
51
|
+
}
|
52
|
+
catch (error) {
|
53
|
+
console.error('Failed to parse Remirror nodes from data-unsupported-node', error);
|
54
|
+
return false;
|
55
|
+
}
|
56
|
+
},
|
57
|
+
},
|
58
|
+
],
|
59
|
+
toDOM: (node) => {
|
60
|
+
const { originalNode, errorMessage, ...rest } = (0, core_1.omitExtraAttributes)(node.attrs, extra);
|
61
|
+
const attrs = {
|
62
|
+
...extra.dom(node),
|
63
|
+
...rest,
|
64
|
+
originalNode,
|
65
|
+
errorMessage,
|
66
|
+
'data-unsupported-node': JSON.stringify({ originalNode, errorMessage }),
|
67
|
+
};
|
68
|
+
return ['div', attrs];
|
69
|
+
},
|
70
|
+
};
|
71
|
+
}
|
72
|
+
};
|
73
|
+
UnsupportedNodeExtension = __decorate([
|
74
|
+
(0, core_1.extension)({})
|
75
|
+
], UnsupportedNodeExtension);
|
76
|
+
exports.UnsupportedNodeExtension = UnsupportedNodeExtension;
|
package/lib/index.css
CHANGED
@@ -350,6 +350,9 @@
|
|
350
350
|
.squiz-fte-scope .visible {
|
351
351
|
visibility: visible !important;
|
352
352
|
}
|
353
|
+
.squiz-fte-scope .collapse {
|
354
|
+
visibility: collapse !important;
|
355
|
+
}
|
353
356
|
.squiz-fte-scope .fixed {
|
354
357
|
position: fixed !important;
|
355
358
|
}
|
@@ -1022,6 +1025,38 @@
|
|
1022
1025
|
.squiz-fte-scope .squiz-fte-checkbox .checkbox svg {
|
1023
1026
|
width: 100%;
|
1024
1027
|
}
|
1028
|
+
.squiz-fte-scope .collapse-box {
|
1029
|
+
border-radius: 0.5rem;
|
1030
|
+
--tw-bg-opacity: 1;
|
1031
|
+
background-color: rgb(245 245 245 / var(--tw-bg-opacity));
|
1032
|
+
padding: 0.5rem;
|
1033
|
+
--tw-text-opacity: 1;
|
1034
|
+
color: rgb(61 61 61 / var(--tw-text-opacity));
|
1035
|
+
}
|
1036
|
+
.squiz-fte-scope .collapse-box__header {
|
1037
|
+
display: flex;
|
1038
|
+
width: 100%;
|
1039
|
+
align-items: center;
|
1040
|
+
}
|
1041
|
+
.squiz-fte-scope .collapse-box__label {
|
1042
|
+
margin-left: 0.5rem;
|
1043
|
+
margin-right: auto;
|
1044
|
+
}
|
1045
|
+
.squiz-fte-scope .collapse-box__icon {
|
1046
|
+
--tw-text-opacity: 1;
|
1047
|
+
color: rgb(112 112 112 / var(--tw-text-opacity));
|
1048
|
+
}
|
1049
|
+
.squiz-fte-scope .collapse-box__icon--warning {
|
1050
|
+
--tw-text-opacity: 1;
|
1051
|
+
color: rgb(215 35 33 / var(--tw-text-opacity));
|
1052
|
+
}
|
1053
|
+
.squiz-fte-scope .collapse-box__content {
|
1054
|
+
margin-top: 0.5rem;
|
1055
|
+
border-radius: 0.5rem;
|
1056
|
+
--tw-bg-opacity: 1;
|
1057
|
+
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
1058
|
+
padding: 0.5rem;
|
1059
|
+
}
|
1025
1060
|
.squiz-fte-scope .squiz-fte-modal {
|
1026
1061
|
display: flex;
|
1027
1062
|
width: 100%;
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
19
|
+
if (mod && mod.__esModule) return mod;
|
20
|
+
var result = {};
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
22
|
+
__setModuleDefault(result, mod);
|
23
|
+
return result;
|
24
|
+
};
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
27
|
+
};
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
29
|
+
const react_1 = __importStar(require("react"));
|
30
|
+
const MotionPhotosOffOutlined_1 = __importDefault(require("@mui/icons-material/MotionPhotosOffOutlined"));
|
31
|
+
const UnfoldLessOutlined_1 = __importDefault(require("@mui/icons-material/UnfoldLessOutlined"));
|
32
|
+
const UnfoldMoreOutlined_1 = __importDefault(require("@mui/icons-material/UnfoldMoreOutlined"));
|
33
|
+
const CollapseBox = ({ node }) => {
|
34
|
+
const [isVisible, setIsVisible] = (0, react_1.useState)(true);
|
35
|
+
const label = 'This section cannot be displayed here due to unsupported HTML elements. The front-end view of your page won’t be affected.';
|
36
|
+
const errorMessage = node?.attrs?.errorMessage;
|
37
|
+
const errorNode = JSON.stringify(node?.attrs?.originalNode, null, 3);
|
38
|
+
return (react_1.default.createElement("div", { className: "collapse-box", suppressContentEditableWarning: true, contentEditable: false },
|
39
|
+
react_1.default.createElement("button", { className: "collapse-box__header", onClick: () => setIsVisible(!isVisible), type: 'button' },
|
40
|
+
react_1.default.createElement(MotionPhotosOffOutlined_1.default, { className: "collapse-box__icon--warning" }),
|
41
|
+
react_1.default.createElement("div", { className: "collapse-box__label" }, label),
|
42
|
+
isVisible ? react_1.default.createElement(UnfoldLessOutlined_1.default, null) : react_1.default.createElement(UnfoldMoreOutlined_1.default, null)),
|
43
|
+
react_1.default.createElement("div", { className: "collapse-box__content", hidden: isVisible, "data-testid": "content" },
|
44
|
+
errorMessage,
|
45
|
+
react_1.default.createElement("br", null),
|
46
|
+
errorNode)));
|
47
|
+
};
|
48
|
+
exports.default = CollapseBox;
|
@@ -105,6 +105,10 @@ const transformNode = (node) => {
|
|
105
105
|
transformedNode = transformMark(mark, transformedNode);
|
106
106
|
}
|
107
107
|
});
|
108
|
+
if (node.type.name === Extensions_1.NodeName.Unsupported) {
|
109
|
+
const unsupportedNode = node.attrs?.originalNode;
|
110
|
+
return { ...unsupportedNode };
|
111
|
+
}
|
108
112
|
return transformedNode;
|
109
113
|
};
|
110
114
|
/**
|
@@ -93,10 +93,17 @@ const getNodeMarks = (node) => {
|
|
93
93
|
});
|
94
94
|
}
|
95
95
|
// Handle font formatting
|
96
|
-
if ('font' in node) {
|
97
|
-
|
98
|
-
|
99
|
-
|
96
|
+
if ('font' in node && node.font !== undefined) {
|
97
|
+
for (const [type, enabled] of Object.entries(node.font)) {
|
98
|
+
if (enabled) {
|
99
|
+
if (type === 'bold' || type === 'italics' || type === 'underline') {
|
100
|
+
marks.push({ type: type === 'italics' ? 'italic' : type });
|
101
|
+
}
|
102
|
+
else {
|
103
|
+
throw new Error(`Unsupported mark provided: ${type}`);
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
100
107
|
}
|
101
108
|
return marks;
|
102
109
|
};
|
@@ -135,7 +142,24 @@ const formatNode = (node) => {
|
|
135
142
|
const squizNodeToRemirrorNode = (nodes) => {
|
136
143
|
let children = [];
|
137
144
|
nodes.forEach((node) => {
|
138
|
-
|
145
|
+
try {
|
146
|
+
children.push(...formatNode(node));
|
147
|
+
}
|
148
|
+
catch (error) {
|
149
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
150
|
+
children.push({
|
151
|
+
type: 'paragraph',
|
152
|
+
content: [
|
153
|
+
{
|
154
|
+
type: Extensions_1.NodeName.Unsupported,
|
155
|
+
attrs: {
|
156
|
+
originalNode: node,
|
157
|
+
errorMessage,
|
158
|
+
},
|
159
|
+
},
|
160
|
+
],
|
161
|
+
});
|
162
|
+
}
|
139
163
|
});
|
140
164
|
if (children.find((child) => child.type === 'text')) {
|
141
165
|
children = [{ type: 'paragraph', content: children }];
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/formatted-text-editor",
|
3
|
-
"version": "1.35.
|
3
|
+
"version": "1.35.1-alpha.1",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"scripts": {
|
@@ -20,8 +20,8 @@
|
|
20
20
|
"@headlessui/react": "1.7.11",
|
21
21
|
"@mui/icons-material": "5.11.16",
|
22
22
|
"@remirror/react": "2.0.25",
|
23
|
-
"@squiz/dx-json-schema-lib": "1.35.
|
24
|
-
"@squiz/resource-browser": "1.35.
|
23
|
+
"@squiz/dx-json-schema-lib": "1.35.1-alpha.1",
|
24
|
+
"@squiz/resource-browser": "1.35.1-alpha.1",
|
25
25
|
"clsx": "1.2.1",
|
26
26
|
"react-hook-form": "7.43.2",
|
27
27
|
"react-image-size": "2.0.0",
|
@@ -75,5 +75,5 @@
|
|
75
75
|
"volta": {
|
76
76
|
"node": "18.15.0"
|
77
77
|
},
|
78
|
-
"gitHead": "
|
78
|
+
"gitHead": "7078d3f12984eca01f1cf0bcef3b63a9509f7511"
|
79
79
|
}
|
@@ -17,12 +17,14 @@ import { EditorContextOptions } from '../Editor/EditorContext';
|
|
17
17
|
import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
|
18
18
|
import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
|
19
19
|
import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
|
20
|
+
import { UnsupportedNodeExtension } from './UnsuportedExtension/UnsupportedNodeExtension';
|
20
21
|
|
21
22
|
export enum NodeName {
|
22
23
|
Image = 'image',
|
23
24
|
CodeBlock = 'codeBlock',
|
24
25
|
AssetImage = 'assetImage',
|
25
26
|
Text = 'text',
|
27
|
+
Unsupported = 'unsupportedNode',
|
26
28
|
}
|
27
29
|
|
28
30
|
export enum MarkName {
|
@@ -52,6 +54,7 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
52
54
|
new AssetLinkExtension({
|
53
55
|
matrixDomain: context.matrix.matrixDomain,
|
54
56
|
}),
|
57
|
+
new UnsupportedNodeExtension(),
|
55
58
|
new ClearFormattingExtension(),
|
56
59
|
];
|
57
60
|
};
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import { ExtensionTag, NodeExtensionSpec, ApplySchemaAttributes } from '@remirror/core';
|
2
|
+
import { NodeName } from '../Extensions';
|
3
|
+
import CollapseBox from '../../ui/CollapseBox/CollapseBox';
|
4
|
+
import { UnsupportedNodeExtension } from './UnsupportedNodeExtension';
|
5
|
+
import { renderWithEditor } from '../../../tests';
|
6
|
+
|
7
|
+
describe('UnsupportedNodeExtension', () => {
|
8
|
+
it('should have the correct extension name', () => {
|
9
|
+
const extension = new UnsupportedNodeExtension();
|
10
|
+
expect(extension.name).toBe(NodeName.Unsupported);
|
11
|
+
});
|
12
|
+
|
13
|
+
it('should use the CollapseBox component as the ReactComponent', () => {
|
14
|
+
const extension = new UnsupportedNodeExtension();
|
15
|
+
expect(extension.ReactComponent).toBe(CollapseBox);
|
16
|
+
});
|
17
|
+
|
18
|
+
it('should create the correct tags', () => {
|
19
|
+
const extension = new UnsupportedNodeExtension();
|
20
|
+
expect(extension.createTags()).toEqual([ExtensionTag.InlineNode]);
|
21
|
+
});
|
22
|
+
|
23
|
+
it('should create the correct node spec', () => {
|
24
|
+
const extension = new UnsupportedNodeExtension();
|
25
|
+
const extra: ApplySchemaAttributes = {
|
26
|
+
defaults: () => ({}),
|
27
|
+
parse: () => ({}),
|
28
|
+
dom: () => ({}),
|
29
|
+
};
|
30
|
+
|
31
|
+
const override: NodeExtensionSpec = {
|
32
|
+
selectable: true,
|
33
|
+
};
|
34
|
+
|
35
|
+
const nodeSpec = extension.createNodeSpec(extra, override);
|
36
|
+
|
37
|
+
expect(nodeSpec.selectable).toBe(true);
|
38
|
+
expect(nodeSpec.draggable).toBe(true);
|
39
|
+
expect(nodeSpec.atom).toBe(true);
|
40
|
+
expect(nodeSpec.inline).toBe(true);
|
41
|
+
expect(nodeSpec.attrs).toEqual({ originalNode: {}, errorMessage: {} });
|
42
|
+
expect(nodeSpec.parseDOM).toEqual([
|
43
|
+
{
|
44
|
+
tag: '[data-unsupported-node]',
|
45
|
+
getAttrs: expect.any(Function),
|
46
|
+
},
|
47
|
+
]);
|
48
|
+
|
49
|
+
expect(nodeSpec.toDOM).toEqual(expect.any(Function));
|
50
|
+
});
|
51
|
+
|
52
|
+
it('should generate the correct toDOM representation', async () => {
|
53
|
+
const { getJsonContent } = await renderWithEditor(null, {
|
54
|
+
content: `<div data-unsupported-node="{"originalNode":{"type":"tag","tag":"p","children":[{"type":"tag","tag":"p","children":[{"type":"text","value":"some text before"},{"type":"sup","children":[{"type":"text","value":"Hello"}]},{"type":"text","value":" and after"},{"type":"tag","tag":"strike","children":[{"type":"text","value":"Some text."}]}]}]},"errorMessage":"Unsupported node type provided: sup"}" />`,
|
55
|
+
});
|
56
|
+
|
57
|
+
expect(getJsonContent()).toEqual({
|
58
|
+
type: 'paragraph',
|
59
|
+
attrs: {
|
60
|
+
nodeIndent: null,
|
61
|
+
nodeTextAlignment: null,
|
62
|
+
nodeLineHeight: null,
|
63
|
+
style: '',
|
64
|
+
},
|
65
|
+
content: [
|
66
|
+
{
|
67
|
+
type: 'unsupportedNode',
|
68
|
+
attrs: {
|
69
|
+
originalNode: {
|
70
|
+
type: 'tag',
|
71
|
+
tag: 'p',
|
72
|
+
children: [
|
73
|
+
{
|
74
|
+
type: 'tag',
|
75
|
+
tag: 'p',
|
76
|
+
children: [
|
77
|
+
{
|
78
|
+
type: 'text',
|
79
|
+
value: 'some text before',
|
80
|
+
},
|
81
|
+
{
|
82
|
+
type: 'sup',
|
83
|
+
children: [
|
84
|
+
{
|
85
|
+
type: 'text',
|
86
|
+
value: 'Hello',
|
87
|
+
},
|
88
|
+
],
|
89
|
+
},
|
90
|
+
{
|
91
|
+
type: 'text',
|
92
|
+
value: ' and after',
|
93
|
+
},
|
94
|
+
{
|
95
|
+
type: 'tag',
|
96
|
+
tag: 'strike',
|
97
|
+
children: [
|
98
|
+
{
|
99
|
+
type: 'text',
|
100
|
+
value: 'Some text.',
|
101
|
+
},
|
102
|
+
],
|
103
|
+
},
|
104
|
+
],
|
105
|
+
},
|
106
|
+
],
|
107
|
+
},
|
108
|
+
errorMessage: 'Unsupported node type provided: sup',
|
109
|
+
},
|
110
|
+
},
|
111
|
+
],
|
112
|
+
});
|
113
|
+
});
|
114
|
+
|
115
|
+
it('should omit extra attributes from node.attrs in toDOM representation', async () => {
|
116
|
+
const { getHtmlContent } = await renderWithEditor(null, {
|
117
|
+
content: {
|
118
|
+
type: 'doc',
|
119
|
+
content: [
|
120
|
+
{
|
121
|
+
type: 'unsupportedNode',
|
122
|
+
content: [
|
123
|
+
{
|
124
|
+
type: 'text',
|
125
|
+
text: 'This is some preformatted text',
|
126
|
+
},
|
127
|
+
],
|
128
|
+
},
|
129
|
+
],
|
130
|
+
},
|
131
|
+
});
|
132
|
+
|
133
|
+
expect(getHtmlContent()).toBe(
|
134
|
+
'<span class="unsupported-node-node-view-wrapper" originalnode="null" errormessage="null" data-unsupported-node="{"originalNode":null,"errorMessage":null}" draggable="true"><div class="collapse-box" contenteditable="false"><button class="collapse-box__header" type="button"><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium collapse-box__icon--warning css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="MotionPhotosOffOutlinedIcon"><path d="M2.81 2.81 1.39 4.22l2.27 2.27C2.61 8.07 2 9.96 2 12c0 5.52 4.48 10 10 10 2.04 0 3.93-.61 5.51-1.66l2.27 2.27 1.41-1.42L2.81 2.81zM12 20c-4.41 0-8-3.59-8-8 0-1.48.41-2.86 1.12-4.06l10.93 10.94C14.86 19.59 13.48 20 12 20zm0-16c4.41 0 8 3.59 8 8 0 1.48-.41 2.86-1.12 4.05l1.45 1.45C21.39 15.93 22 14.04 22 12c0-5.52-4.48-10-10-10-2.04 0-3.93.61-5.51 1.66l1.45 1.45C9.14 4.41 10.52 4 12 4z"></path></svg><div class="collapse-box__label">This section cannot be displayed here due to unsupported HTML elements. The front-end view of your page won’t be affected.</div><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="UnfoldLessOutlinedIcon"><path d="M7.41 18.59 8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z"></path></svg></button><div class="collapse-box__content" hidden="" data-testid="content"><br>null</div></div></span>',
|
135
|
+
);
|
136
|
+
});
|
137
|
+
});
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import {
|
2
|
+
ApplySchemaAttributes,
|
3
|
+
ExtensionTag,
|
4
|
+
NodeExtension,
|
5
|
+
NodeExtensionSpec,
|
6
|
+
NodeSpecOverride,
|
7
|
+
ProsemirrorNode,
|
8
|
+
extension,
|
9
|
+
isElementDomNode,
|
10
|
+
omitExtraAttributes,
|
11
|
+
} from '@remirror/core';
|
12
|
+
|
13
|
+
import { NodeName } from '../Extensions';
|
14
|
+
import { ComponentType } from 'react';
|
15
|
+
import { NodeViewComponentProps } from '@remirror/react';
|
16
|
+
import CollapseBox from '../../ui/CollapseBox/CollapseBox';
|
17
|
+
|
18
|
+
@extension({})
|
19
|
+
export class UnsupportedNodeExtension extends NodeExtension {
|
20
|
+
get name() {
|
21
|
+
return NodeName.Unsupported as const;
|
22
|
+
}
|
23
|
+
|
24
|
+
ReactComponent: ComponentType<NodeViewComponentProps> = CollapseBox;
|
25
|
+
|
26
|
+
createTags() {
|
27
|
+
return [ExtensionTag.InlineNode];
|
28
|
+
}
|
29
|
+
|
30
|
+
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
|
31
|
+
return {
|
32
|
+
selectable: false,
|
33
|
+
draggable: true,
|
34
|
+
atom: true,
|
35
|
+
inline: true,
|
36
|
+
...override,
|
37
|
+
attrs: {
|
38
|
+
...extra.defaults(),
|
39
|
+
originalNode: {},
|
40
|
+
errorMessage: {},
|
41
|
+
},
|
42
|
+
parseDOM: [
|
43
|
+
{
|
44
|
+
tag: `[data-unsupported-node]`,
|
45
|
+
getAttrs: (node) => {
|
46
|
+
if (!isElementDomNode(node)) {
|
47
|
+
return false;
|
48
|
+
}
|
49
|
+
const unsupportedNodes = node.getAttribute('data-unsupported-node');
|
50
|
+
|
51
|
+
try {
|
52
|
+
const decodedNodes = JSON.parse(unsupportedNodes as string);
|
53
|
+
|
54
|
+
return {
|
55
|
+
...extra.parse(node),
|
56
|
+
originalNode: decodedNodes?.originalNode,
|
57
|
+
errorMessage: decodedNodes?.errorMessage,
|
58
|
+
};
|
59
|
+
} catch (error) {
|
60
|
+
console.error('Failed to parse Remirror nodes from data-unsupported-node', error);
|
61
|
+
return false;
|
62
|
+
}
|
63
|
+
},
|
64
|
+
},
|
65
|
+
],
|
66
|
+
toDOM: (node: ProsemirrorNode) => {
|
67
|
+
const { originalNode, errorMessage, ...rest } = omitExtraAttributes(node.attrs, extra);
|
68
|
+
|
69
|
+
const attrs = {
|
70
|
+
...extra.dom(node),
|
71
|
+
...rest,
|
72
|
+
originalNode,
|
73
|
+
errorMessage,
|
74
|
+
'data-unsupported-node': JSON.stringify({ originalNode, errorMessage }),
|
75
|
+
};
|
76
|
+
return ['div', attrs];
|
77
|
+
},
|
78
|
+
};
|
79
|
+
}
|
80
|
+
}
|
package/src/index.scss
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import '@testing-library/jest-dom';
|
4
|
+
import { NodeViewComponentProps } from '@remirror/react';
|
5
|
+
import CollapseBox, { CollapseBoxProps } from './CollapseBox';
|
6
|
+
|
7
|
+
jest.mock('@remirror/react');
|
8
|
+
|
9
|
+
describe('CollapseBox', () => {
|
10
|
+
const createProps = (): CollapseBoxProps => ({
|
11
|
+
node: jest.fn() as unknown as NodeViewComponentProps['node'],
|
12
|
+
});
|
13
|
+
|
14
|
+
const props = createProps();
|
15
|
+
|
16
|
+
it('should display the label correctly', () => {
|
17
|
+
const label =
|
18
|
+
'This section cannot be displayed here due to unsupported HTML elements. The front-end view of your page won’t be affected.';
|
19
|
+
const { getByText } = render(<CollapseBox {...props} />);
|
20
|
+
|
21
|
+
expect(getByText(label)).toBeInTheDocument();
|
22
|
+
});
|
23
|
+
|
24
|
+
it('should display the warning icon correctly', () => {
|
25
|
+
const { getByTestId } = render(<CollapseBox {...props} />);
|
26
|
+
|
27
|
+
expect(getByTestId('MotionPhotosOffOutlinedIcon')).toBeInTheDocument();
|
28
|
+
});
|
29
|
+
|
30
|
+
it('should display the collapse icons correctly', () => {
|
31
|
+
const { getByTestId, getByRole } = render(<CollapseBox {...props} />);
|
32
|
+
|
33
|
+
expect(getByTestId('UnfoldLessOutlinedIcon')).toBeInTheDocument();
|
34
|
+
|
35
|
+
fireEvent.click(getByRole('button'));
|
36
|
+
|
37
|
+
expect(getByTestId('UnfoldMoreOutlinedIcon')).toBeInTheDocument();
|
38
|
+
});
|
39
|
+
|
40
|
+
it('should hide the content when isVisible is false', () => {
|
41
|
+
const { getByTestId, getByRole } = render(<CollapseBox {...props} />);
|
42
|
+
|
43
|
+
expect(getByTestId('content')).not.toBeVisible();
|
44
|
+
|
45
|
+
fireEvent.click(getByRole('button'));
|
46
|
+
|
47
|
+
expect(getByTestId('content')).toBeVisible();
|
48
|
+
});
|
49
|
+
});
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { NodeViewComponentProps } from '@remirror/react';
|
3
|
+
import MotionPhotosOffOutlinedIcon from '@mui/icons-material/MotionPhotosOffOutlined';
|
4
|
+
import UnfoldLessOutlinedIcon from '@mui/icons-material/UnfoldLessOutlined';
|
5
|
+
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
6
|
+
|
7
|
+
export interface CollapseBoxProps {
|
8
|
+
node: NodeViewComponentProps['node'];
|
9
|
+
}
|
10
|
+
|
11
|
+
const CollapseBox: React.FC<CollapseBoxProps> = ({ node }) => {
|
12
|
+
const [isVisible, setIsVisible] = useState(true);
|
13
|
+
|
14
|
+
const label =
|
15
|
+
'This section cannot be displayed here due to unsupported HTML elements. The front-end view of your page won’t be affected.';
|
16
|
+
|
17
|
+
const errorMessage = node?.attrs?.errorMessage;
|
18
|
+
const errorNode = JSON.stringify(node?.attrs?.originalNode, null, 3);
|
19
|
+
|
20
|
+
return (
|
21
|
+
<div className="collapse-box" suppressContentEditableWarning={true} contentEditable={false}>
|
22
|
+
<button className="collapse-box__header" onClick={() => setIsVisible(!isVisible)} type={'button'}>
|
23
|
+
<MotionPhotosOffOutlinedIcon className="collapse-box__icon--warning" />
|
24
|
+
<div className="collapse-box__label">{label}</div>
|
25
|
+
{isVisible ? <UnfoldLessOutlinedIcon /> : <UnfoldMoreOutlinedIcon />}
|
26
|
+
</button>
|
27
|
+
<div className="collapse-box__content" hidden={isVisible} data-testid="content">
|
28
|
+
{errorMessage}
|
29
|
+
<br />
|
30
|
+
{errorNode}
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
);
|
34
|
+
};
|
35
|
+
|
36
|
+
export default CollapseBox;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
.collapse-box {
|
2
|
+
@apply text-gray-800 p-2 bg-gray-100 rounded-lg;
|
3
|
+
|
4
|
+
&__header {
|
5
|
+
@apply flex items-center w-full;
|
6
|
+
}
|
7
|
+
|
8
|
+
&__label {
|
9
|
+
@apply ml-2 mr-auto;
|
10
|
+
}
|
11
|
+
|
12
|
+
&__icon {
|
13
|
+
@apply text-gray-600;
|
14
|
+
|
15
|
+
&--warning {
|
16
|
+
@apply text-red-300;
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
&__content {
|
21
|
+
@apply bg-white mt-2 p-2 rounded-lg;
|
22
|
+
}
|
23
|
+
}
|
@@ -141,6 +141,11 @@ const transformNode = (node: ProsemirrorNode): FormattedNode => {
|
|
141
141
|
}
|
142
142
|
});
|
143
143
|
|
144
|
+
if (node.type.name === NodeName.Unsupported) {
|
145
|
+
const unsupportedNode = node.attrs?.originalNode;
|
146
|
+
return { ...unsupportedNode };
|
147
|
+
}
|
148
|
+
|
144
149
|
return transformedNode;
|
145
150
|
};
|
146
151
|
|
@@ -142,7 +142,32 @@ describe('squizNodeToRemirrorNode', () => {
|
|
142
142
|
tag: 'video',
|
143
143
|
},
|
144
144
|
],
|
145
|
-
|
145
|
+
{
|
146
|
+
type: 'doc',
|
147
|
+
content: [
|
148
|
+
{
|
149
|
+
type: 'paragraph',
|
150
|
+
content: [
|
151
|
+
{
|
152
|
+
type: 'unsupportedNode',
|
153
|
+
attrs: {
|
154
|
+
originalNode: {
|
155
|
+
children: [
|
156
|
+
{
|
157
|
+
type: 'text',
|
158
|
+
value: 'Should throw an error.',
|
159
|
+
},
|
160
|
+
],
|
161
|
+
type: 'tag',
|
162
|
+
tag: 'video',
|
163
|
+
},
|
164
|
+
errorMessage: 'Unsupported node type provided: tag (tag: video)',
|
165
|
+
},
|
166
|
+
},
|
167
|
+
],
|
168
|
+
},
|
169
|
+
],
|
170
|
+
},
|
146
171
|
],
|
147
172
|
[
|
148
173
|
'Unsupported node type',
|
@@ -157,10 +182,35 @@ describe('squizNodeToRemirrorNode', () => {
|
|
157
182
|
type: 'unsupported-type',
|
158
183
|
},
|
159
184
|
],
|
160
|
-
|
185
|
+
{
|
186
|
+
type: 'doc',
|
187
|
+
content: [
|
188
|
+
{
|
189
|
+
type: 'paragraph',
|
190
|
+
content: [
|
191
|
+
{
|
192
|
+
type: 'unsupportedNode',
|
193
|
+
attrs: {
|
194
|
+
originalNode: {
|
195
|
+
children: [
|
196
|
+
{
|
197
|
+
type: 'text',
|
198
|
+
value: 'Should throw an error.',
|
199
|
+
},
|
200
|
+
],
|
201
|
+
type: 'unsupported-type',
|
202
|
+
},
|
203
|
+
errorMessage: 'Unsupported node type provided: unsupported-type',
|
204
|
+
},
|
205
|
+
},
|
206
|
+
],
|
207
|
+
},
|
208
|
+
],
|
209
|
+
},
|
161
210
|
],
|
162
|
-
])('should throw an error for non supported node types', (description: string, node: any,
|
163
|
-
|
211
|
+
])('should throw an error for non supported node types', (description: string, node: any, expected: any) => {
|
212
|
+
const result = squizNodeToRemirrorNode(node);
|
213
|
+
expect(result).toEqual(expected);
|
164
214
|
});
|
165
215
|
|
166
216
|
it('should handle pre formatted text', () => {
|
@@ -104,10 +104,16 @@ const getNodeMarks = (node: FormattedNodes): ObjectMark[] => {
|
|
104
104
|
}
|
105
105
|
|
106
106
|
// Handle font formatting
|
107
|
-
if ('font' in node) {
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
if ('font' in node && node.font !== undefined) {
|
108
|
+
for (const [type, enabled] of Object.entries(node.font)) {
|
109
|
+
if (enabled) {
|
110
|
+
if (type === 'bold' || type === 'italics' || type === 'underline') {
|
111
|
+
marks.push({ type: type === 'italics' ? 'italic' : type });
|
112
|
+
} else {
|
113
|
+
throw new Error(`Unsupported mark provided: ${type}`);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
111
117
|
}
|
112
118
|
|
113
119
|
return marks;
|
@@ -154,7 +160,23 @@ export const squizNodeToRemirrorNode = (nodes: FormattedText): RemirrorJSON => {
|
|
154
160
|
let children: RemirrorJSON[] = [];
|
155
161
|
|
156
162
|
nodes.forEach((node) => {
|
157
|
-
|
163
|
+
try {
|
164
|
+
children.push(...formatNode(node));
|
165
|
+
} catch (error) {
|
166
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
167
|
+
children.push({
|
168
|
+
type: 'paragraph',
|
169
|
+
content: [
|
170
|
+
{
|
171
|
+
type: NodeName.Unsupported,
|
172
|
+
attrs: {
|
173
|
+
originalNode: node,
|
174
|
+
errorMessage,
|
175
|
+
},
|
176
|
+
},
|
177
|
+
],
|
178
|
+
});
|
179
|
+
}
|
158
180
|
});
|
159
181
|
|
160
182
|
if (children.find((child) => child.type === 'text')) {
|
@@ -15,6 +15,6 @@ describe('getNodeNamesByGroup', () => {
|
|
15
15
|
// Nodes in the first array will be transformed to a paragraph when formatting is cleared.
|
16
16
|
// Nodes in the second array will be left as-is.
|
17
17
|
expect(formattingNodeNames).toEqual(['paragraph', 'heading', 'preformatted']);
|
18
|
-
expect(otherNodeNames).toEqual(['assetImage', 'doc', 'text', 'codeBlock', 'image']);
|
18
|
+
expect(otherNodeNames).toEqual(['assetImage', 'doc', 'text', 'codeBlock', 'image', 'unsupportedNode']);
|
19
19
|
});
|
20
20
|
});
|