@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.
@@ -4,7 +4,8 @@ export declare enum NodeName {
4
4
  Image = "image",
5
5
  CodeBlock = "codeBlock",
6
6
  AssetImage = "assetImage",
7
- Text = "text"
7
+ Text = "text",
8
+ Unsupported = "unsupportedNode"
8
9
  }
9
10
  export declare enum MarkName {
10
11
  Link = "link",
@@ -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,7 @@
1
+ import React from 'react';
2
+ import { NodeViewComponentProps } from '@remirror/react';
3
+ export interface CollapseBoxProps {
4
+ node: NodeViewComponentProps['node'];
5
+ }
6
+ declare const CollapseBox: React.FC<CollapseBoxProps>;
7
+ export default CollapseBox;
@@ -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
- node.font?.bold && marks.push({ type: 'bold' });
98
- node.font?.italics && marks.push({ type: 'italic' });
99
- node.font?.underline && marks.push({ type: 'underline' });
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
- children.push(...formatNode(node));
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.0",
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.0",
24
- "@squiz/resource-browser": "1.35.0",
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": "124a9ee3a6880fb8e5207bc9dc4448660e945936"
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="{&quot;originalNode&quot;:{&quot;type&quot;:&quot;tag&quot;,&quot;tag&quot;:&quot;p&quot;,&quot;children&quot;:[{&quot;type&quot;:&quot;tag&quot;,&quot;tag&quot;:&quot;p&quot;,&quot;children&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;value&quot;:&quot;some text before&quot;},{&quot;type&quot;:&quot;sup&quot;,&quot;children&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;value&quot;:&quot;Hello&quot;}]},{&quot;type&quot;:&quot;text&quot;,&quot;value&quot;:&quot; and after&quot;},{&quot;type&quot;:&quot;tag&quot;,&quot;tag&quot;:&quot;strike&quot;,&quot;children&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;value&quot;:&quot;Some text.&quot;}]}]}]},&quot;errorMessage&quot;:&quot;Unsupported node type provided: sup&quot;}" />`,
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="{&quot;originalNode&quot;:null,&quot;errorMessage&quot;: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
@@ -19,5 +19,6 @@
19
19
  @import './ui/ToolbarDropdown/toolbar-dropdown';
20
20
  @import './ui/ToolbarDropdownButton/toolbar-dropdown-button';
21
21
  @import './ui/Fields/Checkbox/checkbox';
22
+ @import './ui/CollapseBox/collapseBox';
22
23
 
23
24
  @import './ui/Modal/modal';
@@ -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
- 'Unsupported node type provided: tag (tag: video)',
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
- 'Unsupported node type provided: unsupported-type',
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, expectedError: string) => {
163
- expect(() => squizNodeToRemirrorNode(node)).toThrow(expectedError);
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
- node.font?.bold && marks.push({ type: 'bold' });
109
- node.font?.italics && marks.push({ type: 'italic' });
110
- node.font?.underline && marks.push({ type: 'underline' });
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
- children.push(...formatNode(node));
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
  });