@squiz/formatted-text-editor 1.34.1-alpha.6 → 1.34.1-alpha.8
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/EditorToolbar/Tools/Image/ImageButton.js +1 -1
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +1 -1
- package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.d.ts +2 -0
- package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.js +22 -0
- package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +3 -2
- package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +7 -1
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +5 -0
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +30 -0
- package/lib/Extensions/Extensions.d.ts +1 -0
- package/lib/Extensions/Extensions.js +3 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +2 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +23 -0
- package/lib/index.css +41 -0
- package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.d.ts +2 -1
- package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +6 -4
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +7 -2
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +7 -0
- package/package.json +4 -4
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +3 -2
- package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.spec.tsx +47 -0
- package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.tsx +32 -0
- package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.spec.tsx +2 -2
- package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.spec.tsx +2 -2
- package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.tsx +3 -1
- package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +11 -1
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +34 -0
- package/src/Extensions/Extensions.ts +3 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +3 -1
- package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +31 -0
- package/src/index.scss +3 -0
- package/src/ui/ToolbarDropdownButton/ToolbarDropdownButton.tsx +8 -4
- package/src/ui/ToolbarDropdownButton/_toolbar-dropdown-button.scss +11 -0
- package/src/ui/_typography.scss +26 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +37 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -2
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +39 -2
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +6 -0
- package/src/utils/getNodeNamesByGroup.spec.ts +1 -1
- package/tailwind.config.cjs +4 -3
@@ -38,7 +38,7 @@ const ImageButton = ({ inPopover = false }) => {
|
|
38
38
|
const active = (0, react_2.useActive)();
|
39
39
|
const selection = (0, react_2.useCurrentSelection)();
|
40
40
|
// if the active selection is not an image, disable the button as it means it will be text
|
41
|
-
const disabled = !selection.empty && !active.image() && !active.assetImage();
|
41
|
+
const disabled = (!selection.empty && !active.image() && !active.assetImage()) || active.codeBlock();
|
42
42
|
const handleClick = () => {
|
43
43
|
if (!showModal) {
|
44
44
|
setShowModal(true);
|
@@ -37,7 +37,7 @@ const LinkButton = ({ inPopover = false }) => {
|
|
37
37
|
const { updateLink, updateAssetLink } = (0, react_2.useCommands)();
|
38
38
|
const active = (0, react_2.useActive)();
|
39
39
|
// If the image tool is active, disable the link tool as they shouldn't work at the same time
|
40
|
-
const disabled = active.image();
|
40
|
+
const disabled = active.image() || active.codeBlock();
|
41
41
|
const handleClick = () => {
|
42
42
|
if (!showModal) {
|
43
43
|
setShowModal(true);
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const react_1 = __importDefault(require("react"));
|
7
|
+
const react_2 = require("@remirror/react");
|
8
|
+
const ToolbarDropdownButton_1 = __importDefault(require("../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton"));
|
9
|
+
const CodeRounded_1 = __importDefault(require("@mui/icons-material/CodeRounded"));
|
10
|
+
const CodeBlockButton = () => {
|
11
|
+
const { toggleCodeBlock } = (0, react_2.useCommands)();
|
12
|
+
const active = (0, react_2.useActive)();
|
13
|
+
const enabled = toggleCodeBlock.enabled();
|
14
|
+
const handleSelect = () => {
|
15
|
+
if (toggleCodeBlock.enabled()) {
|
16
|
+
toggleCodeBlock();
|
17
|
+
}
|
18
|
+
};
|
19
|
+
return (react_1.default.createElement(ToolbarDropdownButton_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.codeBlock(), label: "Code block", icon: react_1.default.createElement(CodeRounded_1.default, null) },
|
20
|
+
react_1.default.createElement("p", null, "Code block")));
|
21
|
+
};
|
22
|
+
exports.default = CodeBlockButton;
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const react_1 = __importDefault(require("react"));
|
7
7
|
const react_2 = require("@remirror/react");
|
8
8
|
const ToolbarDropdownButton_1 = __importDefault(require("../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton"));
|
9
|
+
const ShortTextRounded_1 = __importDefault(require("@mui/icons-material/ShortTextRounded"));
|
9
10
|
const PreformattedButton = () => {
|
10
11
|
const { togglePreformatted } = (0, react_2.useCommands)();
|
11
12
|
const active = (0, react_2.useActive)();
|
@@ -15,7 +16,7 @@ const PreformattedButton = () => {
|
|
15
16
|
togglePreformatted();
|
16
17
|
}
|
17
18
|
};
|
18
|
-
return (react_1.default.createElement(ToolbarDropdownButton_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.preformatted(), label: "Preformatted" },
|
19
|
-
react_1.default.createElement("
|
19
|
+
return (react_1.default.createElement(ToolbarDropdownButton_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.preformatted(), label: "Preformatted", icon: react_1.default.createElement(ShortTextRounded_1.default, null) },
|
20
|
+
react_1.default.createElement("p", null, "Preformatted")));
|
20
21
|
};
|
21
22
|
exports.default = PreformattedButton;
|
@@ -7,6 +7,7 @@ const react_1 = __importDefault(require("react"));
|
|
7
7
|
const HeadingButton_1 = __importDefault(require("./Heading/HeadingButton"));
|
8
8
|
const ParagraphButton_1 = __importDefault(require("./Paragraph/ParagraphButton"));
|
9
9
|
const PreformattedButton_1 = __importDefault(require("./Preformatted/PreformattedButton"));
|
10
|
+
const CodeBlockButton_1 = __importDefault(require("./CodeBlock/CodeBlockButton"));
|
10
11
|
const ToolbarDropdown_1 = __importDefault(require("../../../ui/ToolbarDropdown/ToolbarDropdown"));
|
11
12
|
const react_2 = require("@remirror/react");
|
12
13
|
const TextTypeDropdown = () => {
|
@@ -16,6 +17,10 @@ const TextTypeDropdown = () => {
|
|
16
17
|
if (active.preformatted()) {
|
17
18
|
return 'Preformatted';
|
18
19
|
}
|
20
|
+
// Determine if codeblock is active
|
21
|
+
if (active.codeBlock()) {
|
22
|
+
return 'Code block';
|
23
|
+
}
|
19
24
|
// Determine if a heading is active
|
20
25
|
for (let i = 1; i <= 6; i++) {
|
21
26
|
if (active.heading({ level: i })) {
|
@@ -34,7 +39,8 @@ const TextTypeDropdown = () => {
|
|
34
39
|
react_1.default.createElement(HeadingButton_1.default, { level: 4 }),
|
35
40
|
react_1.default.createElement(HeadingButton_1.default, { level: 5 }),
|
36
41
|
react_1.default.createElement(HeadingButton_1.default, { level: 6 }),
|
37
|
-
react_1.default.createElement(PreformattedButton_1.default, null)
|
42
|
+
react_1.default.createElement(PreformattedButton_1.default, null),
|
43
|
+
react_1.default.createElement(CodeBlockButton_1.default, null)),
|
38
44
|
react_1.default.createElement(react_2.VerticalDivider, null)));
|
39
45
|
};
|
40
46
|
exports.default = TextTypeDropdown;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.ExtendedCodeBlockExtension = void 0;
|
4
|
+
const extension_code_block_1 = require("@remirror/extension-code-block");
|
5
|
+
class ExtendedCodeBlockExtension extends extension_code_block_1.CodeBlockExtension {
|
6
|
+
createNodeViews() {
|
7
|
+
return (node) => {
|
8
|
+
const { language } = node.attrs;
|
9
|
+
// This is the pre container for the code block
|
10
|
+
const dom = document.createElement('pre');
|
11
|
+
dom.setAttribute('spellcheck', 'false');
|
12
|
+
dom.classList.add(`code-block`);
|
13
|
+
// This is the actual code content in the code block
|
14
|
+
const contentDOM = document.createElement('code');
|
15
|
+
contentDOM.setAttribute('data-code-block-language', language);
|
16
|
+
// Divider between code block and pre container
|
17
|
+
const dividerElement = document.createElement('div');
|
18
|
+
dividerElement.classList.add('block-divider');
|
19
|
+
// The material icon to use
|
20
|
+
const codeIcon = document.createElement('svg');
|
21
|
+
codeIcon.classList.add('material-symbols-rounded');
|
22
|
+
codeIcon.textContent = 'code';
|
23
|
+
dom.append(codeIcon);
|
24
|
+
dom.append(dividerElement);
|
25
|
+
dom.append(contentDOM);
|
26
|
+
return { dom, contentDOM };
|
27
|
+
};
|
28
|
+
}
|
29
|
+
}
|
30
|
+
exports.ExtendedCodeBlockExtension = ExtendedCodeBlockExtension;
|
@@ -8,10 +8,12 @@ const LinkExtension_1 = require("./LinkExtension/LinkExtension");
|
|
8
8
|
const ImageExtension_1 = require("./ImageExtension/ImageExtension");
|
9
9
|
const CommandsExtension_1 = require("./CommandsExtension/CommandsExtension");
|
10
10
|
const AssetImageExtension_1 = require("./ImageExtension/AssetImageExtension");
|
11
|
+
const CodeBlockExtension_1 = require("./CodeBlockExtension/CodeBlockExtension");
|
11
12
|
const ClearFormattingExtension_1 = require("./ClearFormattingExtension/ClearFormattingExtension");
|
12
13
|
var NodeName;
|
13
14
|
(function (NodeName) {
|
14
15
|
NodeName["Image"] = "image";
|
16
|
+
NodeName["CodeBlock"] = "codeBlock";
|
15
17
|
NodeName["AssetImage"] = "assetImage";
|
16
18
|
NodeName["Text"] = "text";
|
17
19
|
})(NodeName = exports.NodeName || (exports.NodeName = {}));
|
@@ -30,6 +32,7 @@ const createExtensions = (context) => {
|
|
30
32
|
new extensions_1.NodeFormattingExtension({ indents: [] }),
|
31
33
|
new extensions_1.ParagraphExtension(),
|
32
34
|
new PreformattedExtension_1.PreformattedExtension(),
|
35
|
+
new CodeBlockExtension_1.ExtendedCodeBlockExtension({ defaultWrap: true }),
|
33
36
|
new extensions_1.UnderlineExtension(),
|
34
37
|
new extensions_1.HistoryExtension(),
|
35
38
|
new ImageExtension_1.ImageExtension(),
|
@@ -1,8 +1,10 @@
|
|
1
1
|
import { ApplySchemaAttributes, CommandFunction, NodeExtension, NodeExtensionSpec, NodeSpecOverride } from '@remirror/core';
|
2
|
+
import { NodeViewMethod } from 'remirror';
|
2
3
|
export declare class PreformattedExtension extends NodeExtension {
|
3
4
|
get name(): "preformatted";
|
4
5
|
createTags(): ("block" | "formattingNode" | "textBlock")[];
|
5
6
|
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec;
|
7
|
+
createNodeViews(): NodeViewMethod;
|
6
8
|
/**
|
7
9
|
* Toggle the <pre> for the current block.
|
8
10
|
*/
|
@@ -34,6 +34,29 @@ let PreformattedExtension = class PreformattedExtension extends core_1.NodeExten
|
|
34
34
|
},
|
35
35
|
};
|
36
36
|
}
|
37
|
+
createNodeViews() {
|
38
|
+
return (node) => {
|
39
|
+
const { nodeTextAlignment } = node.attrs;
|
40
|
+
// This is the pre container for the code block
|
41
|
+
const dom = document.createElement('div');
|
42
|
+
dom.classList.add(`preformatted`);
|
43
|
+
// This is the actual code content in the code block
|
44
|
+
const contentDOM = document.createElement('pre');
|
45
|
+
contentDOM.setAttribute('data-node-text-align', nodeTextAlignment);
|
46
|
+
contentDOM.setAttribute('style', `text-align:${nodeTextAlignment}`);
|
47
|
+
// Divider between code block and pre container
|
48
|
+
const dividerElement = document.createElement('div');
|
49
|
+
dividerElement.classList.add('block-divider');
|
50
|
+
// The material icon to use
|
51
|
+
const codeIcon = document.createElement('svg');
|
52
|
+
codeIcon.classList.add('material-symbols-rounded');
|
53
|
+
codeIcon.textContent = 'short_text';
|
54
|
+
dom.append(codeIcon);
|
55
|
+
dom.append(dividerElement);
|
56
|
+
dom.append(contentDOM);
|
57
|
+
return { dom, contentDOM };
|
58
|
+
};
|
59
|
+
}
|
37
60
|
/**
|
38
61
|
* Toggle the <pre> for the current block.
|
39
62
|
*/
|
package/lib/index.css
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
@import "https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded";
|
2
|
+
|
1
3
|
/* src/index.scss */
|
2
4
|
.squiz-fte-scope *,
|
3
5
|
.squiz-fte-scope ::before,
|
@@ -674,6 +676,36 @@
|
|
674
676
|
letter-spacing: -0.2px;
|
675
677
|
line-height: 1.25rem;
|
676
678
|
}
|
679
|
+
.squiz-fte-scope .code-block,
|
680
|
+
.squiz-fte-scope .preformatted {
|
681
|
+
display: flex;
|
682
|
+
}
|
683
|
+
.squiz-fte-scope .code-block .material-symbols-rounded,
|
684
|
+
.squiz-fte-scope .preformatted .material-symbols-rounded {
|
685
|
+
font-size: 1.125rem;
|
686
|
+
--tw-text-opacity: 1;
|
687
|
+
color: rgb(79 79 79 / var(--tw-text-opacity));
|
688
|
+
pointer-events: none;
|
689
|
+
margin-right: 0.375rem;
|
690
|
+
}
|
691
|
+
.squiz-fte-scope .code-block .block-divider,
|
692
|
+
.squiz-fte-scope .preformatted .block-divider {
|
693
|
+
--tw-bg-opacity: 1;
|
694
|
+
background-color: rgb(237 237 237 / var(--tw-bg-opacity));
|
695
|
+
width: 4px;
|
696
|
+
border-radius: 0.75rem;
|
697
|
+
margin-right: 0.625rem;
|
698
|
+
}
|
699
|
+
.squiz-fte-scope .code-block code,
|
700
|
+
.squiz-fte-scope .code-block pre,
|
701
|
+
.squiz-fte-scope .preformatted code,
|
702
|
+
.squiz-fte-scope .preformatted pre {
|
703
|
+
--tw-text-opacity: 1;
|
704
|
+
color: rgb(79 79 79 / var(--tw-text-opacity));
|
705
|
+
font-size: 0.75rem;
|
706
|
+
padding-top: 0.75rem;
|
707
|
+
padding-bottom: 0.75rem;
|
708
|
+
}
|
677
709
|
.squiz-fte-scope .squiz-fte-form-group {
|
678
710
|
display: flex;
|
679
711
|
flex-direction: column;
|
@@ -959,6 +991,15 @@
|
|
959
991
|
.squiz-fte-scope .dropdown-button:focus {
|
960
992
|
background-color: rgba(0, 0, 0, 0.04);
|
961
993
|
}
|
994
|
+
.squiz-fte-scope .dropdown-button svg {
|
995
|
+
font-size: 18px;
|
996
|
+
}
|
997
|
+
.squiz-fte-scope .dropdown-button .dropdown-button-label {
|
998
|
+
display: flex;
|
999
|
+
align-items: center;
|
1000
|
+
font-size: 14px;
|
1001
|
+
gap: 5px;
|
1002
|
+
}
|
962
1003
|
.squiz-fte-scope .squiz-fte-checkbox {
|
963
1004
|
--tw-text-opacity: 1;
|
964
1005
|
color: rgb(61 61 61 / var(--tw-text-opacity));
|
@@ -4,6 +4,7 @@ type DropdownButtonProps = {
|
|
4
4
|
isDisabled: boolean;
|
5
5
|
isActive: boolean;
|
6
6
|
label: string;
|
7
|
+
icon?: JSX.Element;
|
7
8
|
};
|
8
|
-
declare const DropdownButton: ({ children, handleOnClick, isDisabled, isActive, label }: DropdownButtonProps) => JSX.Element;
|
9
|
+
declare const DropdownButton: ({ children, handleOnClick, isDisabled, isActive, label, icon }: DropdownButtonProps) => JSX.Element;
|
9
10
|
export default DropdownButton;
|
@@ -4,10 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
const react_1 = __importDefault(require("react"));
|
7
|
-
const
|
8
|
-
const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }) => {
|
7
|
+
const CheckRounded_1 = __importDefault(require("@mui/icons-material/CheckRounded"));
|
8
|
+
const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label, icon }) => {
|
9
9
|
return (react_1.default.createElement("button", { "aria-label": label, id: "dropdownMenuButton", title: label, type: "button", onClick: handleOnClick, disabled: isDisabled, className: `btn dropdown-button ${isActive ? 'is-active' : ''}` },
|
10
|
-
react_1.default.createElement("
|
11
|
-
|
10
|
+
react_1.default.createElement("div", { className: "dropdown-button-label" },
|
11
|
+
icon && icon,
|
12
|
+
children || label),
|
13
|
+
isActive && react_1.default.createElement(CheckRounded_1.default, { className: "dropdown-button-icon" })));
|
12
14
|
};
|
13
15
|
exports.default = DropdownButton;
|
@@ -4,9 +4,12 @@ exports.remirrorNodeToSquizNode = exports.resolveNodeTag = void 0;
|
|
4
4
|
const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
|
5
5
|
const Extensions_1 = require("../../../Extensions/Extensions");
|
6
6
|
const resolveNodeTag = (node) => {
|
7
|
-
if (node.type.name ===
|
7
|
+
if (node.type.name === Extensions_1.NodeName.Text) {
|
8
8
|
return 'span';
|
9
9
|
}
|
10
|
+
if (node.type.name === Extensions_1.NodeName.CodeBlock) {
|
11
|
+
return 'code';
|
12
|
+
}
|
10
13
|
if (node.type.spec?.toDOM) {
|
11
14
|
const domNode = node.type.spec.toDOM(node);
|
12
15
|
if (domNode instanceof window.Node) {
|
@@ -63,9 +66,11 @@ const transformFragment = (fragment) => {
|
|
63
66
|
return transformed;
|
64
67
|
};
|
65
68
|
const transformNode = (node) => {
|
66
|
-
const attributes = node.type.name === Extensions_1.NodeName.Image ? transformAttributes(node.attrs) : undefined;
|
67
69
|
const formattingOptions = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFormattingOptions(node));
|
68
70
|
const font = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFontOptions(node));
|
71
|
+
const attributes = node.type.name === Extensions_1.NodeName.Image || node.type.name === Extensions_1.NodeName.CodeBlock
|
72
|
+
? transformAttributes(node.attrs)
|
73
|
+
: undefined;
|
69
74
|
let transformedNode = { type: 'text', value: node.text || '' };
|
70
75
|
// Squiz "text" nodes can't have formatting/font options but Remirror "text" nodes can.
|
71
76
|
// If the node has formatting options wrap in a tag.
|
@@ -21,6 +21,7 @@ const getNodeType = (node) => {
|
|
21
21
|
p: 'paragraph',
|
22
22
|
a: Extensions_1.NodeName.Text,
|
23
23
|
span: Extensions_1.NodeName.Text,
|
24
|
+
code: Extensions_1.NodeName.CodeBlock,
|
24
25
|
};
|
25
26
|
if (typeMap[node.type]) {
|
26
27
|
return typeMap[node.type];
|
@@ -43,6 +44,12 @@ const getNodeAttributes = (node) => {
|
|
43
44
|
title: node.attributes?.title,
|
44
45
|
};
|
45
46
|
}
|
47
|
+
else if (node.type === 'tag' && node.tag === 'code') {
|
48
|
+
return {
|
49
|
+
language: node.attributes?.language || 'markup',
|
50
|
+
wrap: node.attributes?.wrap || true,
|
51
|
+
};
|
52
|
+
}
|
46
53
|
else if (node.type === 'matrix-image') {
|
47
54
|
return {
|
48
55
|
matrixAssetId: node.matrixAssetId,
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/formatted-text-editor",
|
3
|
-
"version": "1.34.1-alpha.
|
3
|
+
"version": "1.34.1-alpha.8",
|
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.34.1-alpha.
|
24
|
-
"@squiz/resource-browser": "1.34.1-alpha.
|
23
|
+
"@squiz/dx-json-schema-lib": "1.34.1-alpha.8",
|
24
|
+
"@squiz/resource-browser": "1.34.1-alpha.8",
|
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": "6e1be2071c856fe3e6e4b14dbc1e67d93b97e12b"
|
79
79
|
}
|
@@ -7,6 +7,7 @@ import Button from '../../../ui/Button/Button';
|
|
7
7
|
import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
|
8
8
|
import { NodeName } from '../../../Extensions/Extensions';
|
9
9
|
import { AssetImageExtension } from '../../../Extensions/ImageExtension/AssetImageExtension';
|
10
|
+
import { CodeBlockExtension } from 'remirror/dist-types/extensions';
|
10
11
|
|
11
12
|
type ImageButtonProps = {
|
12
13
|
inPopover?: boolean;
|
@@ -15,10 +16,10 @@ type ImageButtonProps = {
|
|
15
16
|
const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
|
16
17
|
const [showModal, setShowModal] = useState(false);
|
17
18
|
const { insertImage, insertAssetImage } = useCommands<ImageExtension | AssetImageExtension>();
|
18
|
-
const active = useActive<ImageExtension | AssetImageExtension>();
|
19
|
+
const active = useActive<ImageExtension | AssetImageExtension | CodeBlockExtension>();
|
19
20
|
const selection = useCurrentSelection();
|
20
21
|
// if the active selection is not an image, disable the button as it means it will be text
|
21
|
-
const disabled = !selection.empty && !active.image() && !active.assetImage();
|
22
|
+
const disabled = (!selection.empty && !active.image() && !active.assetImage()) || active.codeBlock();
|
22
23
|
|
23
24
|
const handleClick = () => {
|
24
25
|
if (!showModal) {
|
@@ -9,6 +9,7 @@ import { CommandsExtension } from '../../../Extensions/CommandsExtension/Command
|
|
9
9
|
import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkExtension';
|
10
10
|
import { MarkName } from '../../../Extensions/Extensions';
|
11
11
|
import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
|
12
|
+
import { CodeBlockExtension } from 'remirror/dist-types/extensions';
|
12
13
|
|
13
14
|
export type LinkButtonProps = {
|
14
15
|
inPopover?: boolean;
|
@@ -17,9 +18,9 @@ export type LinkButtonProps = {
|
|
17
18
|
const LinkButton = ({ inPopover = false }: LinkButtonProps) => {
|
18
19
|
const [showModal, setShowModal] = useState(false);
|
19
20
|
const { updateLink, updateAssetLink } = useCommands<AssetLinkExtension | LinkExtension | CommandsExtension>();
|
20
|
-
const active = useActive<LinkExtension | AssetLinkExtension | ImageExtension>();
|
21
|
+
const active = useActive<LinkExtension | AssetLinkExtension | ImageExtension | CodeBlockExtension>();
|
21
22
|
// If the image tool is active, disable the link tool as they shouldn't work at the same time
|
22
|
-
const disabled = active.image();
|
23
|
+
const disabled = active.image() || active.codeBlock();
|
23
24
|
const handleClick = () => {
|
24
25
|
if (!showModal) {
|
25
26
|
setShowModal(true);
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import Editor from '../../../../Editor/Editor';
|
4
|
+
import React from 'react';
|
5
|
+
|
6
|
+
describe('Code block button', () => {
|
7
|
+
it('Renders the code block button', () => {
|
8
|
+
const { baseElement, getByRole } = render(<Editor />);
|
9
|
+
expect(baseElement).toBeTruthy();
|
10
|
+
expect(getByRole('button', { name: 'Code block' })).toBeTruthy();
|
11
|
+
});
|
12
|
+
|
13
|
+
it('Applies active status after selecting code block button', () => {
|
14
|
+
const { baseElement, getByRole } = render(<Editor />);
|
15
|
+
expect(baseElement).toBeTruthy();
|
16
|
+
|
17
|
+
const codeblockButton = getByRole('button', { name: 'Code block' });
|
18
|
+
expect(codeblockButton).toBeTruthy();
|
19
|
+
expect(codeblockButton.className).not.toContain('is-active');
|
20
|
+
|
21
|
+
fireEvent.click(codeblockButton);
|
22
|
+
expect(codeblockButton.className).toContain('is-active');
|
23
|
+
});
|
24
|
+
|
25
|
+
it('Should render a check icon if button is active', () => {
|
26
|
+
const { baseElement, getByRole } = render(<Editor />);
|
27
|
+
expect(baseElement).toBeTruthy();
|
28
|
+
|
29
|
+
const codeblockButton = getByRole('button', { name: 'Code block' });
|
30
|
+
expect(codeblockButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
|
31
|
+
|
32
|
+
fireEvent.click(codeblockButton);
|
33
|
+
expect(codeblockButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
|
34
|
+
});
|
35
|
+
|
36
|
+
it('Should apply preformatted tag and code tag to editor after clicking button', () => {
|
37
|
+
const { baseElement, getByRole } = render(<Editor />);
|
38
|
+
expect(baseElement).toBeTruthy();
|
39
|
+
expect(baseElement.querySelector('div.remirror-editor pre code')).toBeFalsy();
|
40
|
+
|
41
|
+
const codeblockButton = getByRole('button', { name: 'Code block' });
|
42
|
+
expect(codeblockButton).toBeTruthy();
|
43
|
+
fireEvent.click(codeblockButton);
|
44
|
+
|
45
|
+
expect(baseElement.querySelector(`div.remirror-editor pre code`)).toBeTruthy();
|
46
|
+
});
|
47
|
+
});
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { useCommands, useActive } from '@remirror/react';
|
3
|
+
import { ExtendedCodeBlockExtension } from '../../../../Extensions/CodeBlockExtension/CodeBlockExtension';
|
4
|
+
import DropdownButton from '../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton';
|
5
|
+
import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
|
6
|
+
|
7
|
+
const CodeBlockButton = () => {
|
8
|
+
const { toggleCodeBlock } = useCommands<ExtendedCodeBlockExtension>();
|
9
|
+
|
10
|
+
const active = useActive<ExtendedCodeBlockExtension>();
|
11
|
+
const enabled = toggleCodeBlock.enabled();
|
12
|
+
|
13
|
+
const handleSelect = () => {
|
14
|
+
if (toggleCodeBlock.enabled()) {
|
15
|
+
toggleCodeBlock();
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
return (
|
20
|
+
<DropdownButton
|
21
|
+
handleOnClick={handleSelect}
|
22
|
+
isDisabled={!enabled}
|
23
|
+
isActive={active.codeBlock()}
|
24
|
+
label="Code block"
|
25
|
+
icon={<CodeRoundedIcon />}
|
26
|
+
>
|
27
|
+
<p>Code block</p>
|
28
|
+
</DropdownButton>
|
29
|
+
);
|
30
|
+
};
|
31
|
+
|
32
|
+
export default CodeBlockButton;
|
@@ -36,10 +36,10 @@ describe('Heading button', () => {
|
|
36
36
|
expect(baseElement).toBeTruthy();
|
37
37
|
|
38
38
|
const headingButton = getByRole('button', { name: label });
|
39
|
-
expect(headingButton.querySelector('svg[data-testid="
|
39
|
+
expect(headingButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
|
40
40
|
|
41
41
|
fireEvent.click(headingButton);
|
42
|
-
expect(headingButton.querySelector('svg[data-testid="
|
42
|
+
expect(headingButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
|
43
43
|
});
|
44
44
|
|
45
45
|
it.each(headings)('Should apply "%s" heading tag to editor after clicking button', (label, tag) => {
|
@@ -25,6 +25,6 @@ describe('Paragraph button', () => {
|
|
25
25
|
|
26
26
|
const paragraphButton = baseElement.querySelector('button[title="Paragraph"]') as HTMLButtonElement;
|
27
27
|
expect(paragraphButton).toBeTruthy();
|
28
|
-
expect(paragraphButton.querySelector('svg[data-testid="
|
28
|
+
expect(paragraphButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
|
29
29
|
});
|
30
30
|
});
|
@@ -27,10 +27,10 @@ describe('Preformatted button', () => {
|
|
27
27
|
expect(baseElement).toBeTruthy();
|
28
28
|
|
29
29
|
const preformattedButton = getByRole('button', { name: 'Preformatted' });
|
30
|
-
expect(preformattedButton.querySelector('svg[data-testid="
|
30
|
+
expect(preformattedButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
|
31
31
|
|
32
32
|
fireEvent.click(preformattedButton);
|
33
|
-
expect(preformattedButton.querySelector('svg[data-testid="
|
33
|
+
expect(preformattedButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
|
34
34
|
});
|
35
35
|
|
36
36
|
it('Should apply preformatted tag to editor after clicking button', () => {
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
2
2
|
import { useCommands, useActive } from '@remirror/react';
|
3
3
|
import { PreformattedExtension } from '../../../../Extensions/PreformattedExtension/PreformattedExtension';
|
4
4
|
import DropdownButton from '../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton';
|
5
|
+
import ShortTextRoundedIcon from '@mui/icons-material/ShortTextRounded';
|
5
6
|
|
6
7
|
const PreformattedButton = () => {
|
7
8
|
const { togglePreformatted } = useCommands<PreformattedExtension>();
|
@@ -21,8 +22,9 @@ const PreformattedButton = () => {
|
|
21
22
|
isDisabled={!enabled}
|
22
23
|
isActive={active.preformatted()}
|
23
24
|
label="Preformatted"
|
25
|
+
icon={<ShortTextRoundedIcon />}
|
24
26
|
>
|
25
|
-
<
|
27
|
+
<p>Preformatted</p>
|
26
28
|
</DropdownButton>
|
27
29
|
);
|
28
30
|
};
|
@@ -2,24 +2,33 @@ import React from 'react';
|
|
2
2
|
import HeadingButton from './Heading/HeadingButton';
|
3
3
|
import ParagraphButton from './Paragraph/ParagraphButton';
|
4
4
|
import PreformattedButton from './Preformatted/PreformattedButton';
|
5
|
+
import CodeBlockButton from './CodeBlock/CodeBlockButton';
|
5
6
|
import ToolbarDropdown from '../../../ui/ToolbarDropdown/ToolbarDropdown';
|
6
7
|
import { useActive, VerticalDivider } from '@remirror/react';
|
7
8
|
import { PreformattedExtension } from '../../../Extensions/PreformattedExtension/PreformattedExtension';
|
9
|
+
import { CodeBlockExtension } from 'remirror/dist-types/extensions';
|
8
10
|
|
9
11
|
const TextTypeDropdown = () => {
|
10
|
-
const active = useActive<PreformattedExtension>();
|
12
|
+
const active = useActive<PreformattedExtension | CodeBlockExtension>();
|
11
13
|
|
12
14
|
const activeLabel = () => {
|
13
15
|
// Determine if preformatted is active
|
14
16
|
if (active.preformatted()) {
|
15
17
|
return 'Preformatted';
|
16
18
|
}
|
19
|
+
|
20
|
+
// Determine if codeblock is active
|
21
|
+
if (active.codeBlock()) {
|
22
|
+
return 'Code block';
|
23
|
+
}
|
24
|
+
|
17
25
|
// Determine if a heading is active
|
18
26
|
for (let i = 1; i <= 6; i++) {
|
19
27
|
if (active.heading({ level: i })) {
|
20
28
|
return `Heading ${i}`;
|
21
29
|
}
|
22
30
|
}
|
31
|
+
|
23
32
|
// Default to paragraph
|
24
33
|
return 'Paragraph';
|
25
34
|
};
|
@@ -35,6 +44,7 @@ const TextTypeDropdown = () => {
|
|
35
44
|
<HeadingButton level={5} />
|
36
45
|
<HeadingButton level={6} />
|
37
46
|
<PreformattedButton />
|
47
|
+
<CodeBlockButton />
|
38
48
|
</ToolbarDropdown>
|
39
49
|
<VerticalDivider />
|
40
50
|
</>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import { CodeBlockExtension } from '@remirror/extension-code-block';
|
2
|
+
import { NodeViewMethod, ProsemirrorNode } from 'remirror';
|
3
|
+
|
4
|
+
export class ExtendedCodeBlockExtension extends CodeBlockExtension {
|
5
|
+
createNodeViews(): NodeViewMethod {
|
6
|
+
return (node: ProsemirrorNode) => {
|
7
|
+
const { language } = node.attrs;
|
8
|
+
|
9
|
+
// This is the pre container for the code block
|
10
|
+
const dom = document.createElement('pre');
|
11
|
+
dom.setAttribute('spellcheck', 'false');
|
12
|
+
dom.classList.add(`code-block`);
|
13
|
+
|
14
|
+
// This is the actual code content in the code block
|
15
|
+
const contentDOM = document.createElement('code');
|
16
|
+
contentDOM.setAttribute('data-code-block-language', language);
|
17
|
+
|
18
|
+
// Divider between code block and pre container
|
19
|
+
const dividerElement = document.createElement('div');
|
20
|
+
dividerElement.classList.add('block-divider');
|
21
|
+
|
22
|
+
// The material icon to use
|
23
|
+
const codeIcon = document.createElement('svg');
|
24
|
+
codeIcon.classList.add('material-symbols-rounded');
|
25
|
+
codeIcon.textContent = 'code';
|
26
|
+
|
27
|
+
dom.append(codeIcon);
|
28
|
+
dom.append(dividerElement);
|
29
|
+
dom.append(contentDOM);
|
30
|
+
|
31
|
+
return { dom, contentDOM };
|
32
|
+
};
|
33
|
+
}
|
34
|
+
}
|
@@ -15,10 +15,12 @@ import { ImageExtension } from './ImageExtension/ImageExtension';
|
|
15
15
|
import { CommandsExtension } from './CommandsExtension/CommandsExtension';
|
16
16
|
import { EditorContextOptions } from '../Editor/EditorContext';
|
17
17
|
import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
|
18
|
+
import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
|
18
19
|
import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
|
19
20
|
|
20
21
|
export enum NodeName {
|
21
22
|
Image = 'image',
|
23
|
+
CodeBlock = 'codeBlock',
|
22
24
|
AssetImage = 'assetImage',
|
23
25
|
Text = 'text',
|
24
26
|
}
|
@@ -38,6 +40,7 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
38
40
|
new NodeFormattingExtension({ indents: [] }),
|
39
41
|
new ParagraphExtension(),
|
40
42
|
new PreformattedExtension(),
|
43
|
+
new ExtendedCodeBlockExtension({ defaultWrap: true }),
|
41
44
|
new UnderlineExtension(),
|
42
45
|
new HistoryExtension(),
|
43
46
|
new ImageExtension(),
|
@@ -36,6 +36,8 @@ describe('PreformattedExtension', () => {
|
|
36
36
|
},
|
37
37
|
});
|
38
38
|
|
39
|
-
expect(getHtmlContent()).toBe(
|
39
|
+
expect(getHtmlContent()).toBe(
|
40
|
+
'<div class="preformatted"><svg class="material-symbols-rounded">short_text</svg><div class="block-divider"></div><pre data-node-text-align="null" style="text-align:null">This is some preformatted text</pre></div>',
|
41
|
+
);
|
40
42
|
});
|
41
43
|
});
|
@@ -10,6 +10,7 @@ import {
|
|
10
10
|
ProsemirrorNode,
|
11
11
|
toggleBlockItem,
|
12
12
|
} from '@remirror/core';
|
13
|
+
import { NodeViewMethod } from 'remirror';
|
13
14
|
|
14
15
|
@extension({})
|
15
16
|
export class PreformattedExtension extends NodeExtension {
|
@@ -41,6 +42,36 @@ export class PreformattedExtension extends NodeExtension {
|
|
41
42
|
};
|
42
43
|
}
|
43
44
|
|
45
|
+
createNodeViews(): NodeViewMethod {
|
46
|
+
return (node: ProsemirrorNode) => {
|
47
|
+
const { nodeTextAlignment } = node.attrs;
|
48
|
+
|
49
|
+
// This is the pre container for the code block
|
50
|
+
const dom = document.createElement('div');
|
51
|
+
dom.classList.add(`preformatted`);
|
52
|
+
|
53
|
+
// This is the actual code content in the code block
|
54
|
+
const contentDOM = document.createElement('pre');
|
55
|
+
contentDOM.setAttribute('data-node-text-align', nodeTextAlignment);
|
56
|
+
contentDOM.setAttribute('style', `text-align:${nodeTextAlignment}`);
|
57
|
+
|
58
|
+
// Divider between code block and pre container
|
59
|
+
const dividerElement = document.createElement('div');
|
60
|
+
dividerElement.classList.add('block-divider');
|
61
|
+
|
62
|
+
// The material icon to use
|
63
|
+
const codeIcon = document.createElement('svg');
|
64
|
+
codeIcon.classList.add('material-symbols-rounded');
|
65
|
+
codeIcon.textContent = 'short_text';
|
66
|
+
|
67
|
+
dom.append(codeIcon);
|
68
|
+
dom.append(dividerElement);
|
69
|
+
dom.append(contentDOM);
|
70
|
+
|
71
|
+
return { dom, contentDOM };
|
72
|
+
};
|
73
|
+
}
|
74
|
+
|
44
75
|
/**
|
45
76
|
* Toggle the <pre> for the current block.
|
46
77
|
*/
|
package/src/index.scss
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
@import 'tailwindcss/components';
|
4
4
|
@import 'tailwindcss/utilities';
|
5
5
|
|
6
|
+
/* So we can use icons inside of FTE content */
|
7
|
+
@import 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded';
|
8
|
+
|
6
9
|
/* Global */
|
7
10
|
@import './ui/typography';
|
8
11
|
@import './ui/forms';
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import
|
2
|
+
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
3
3
|
|
4
4
|
type DropdownButtonProps = {
|
5
5
|
children?: JSX.Element;
|
@@ -7,9 +7,10 @@ type DropdownButtonProps = {
|
|
7
7
|
isDisabled: boolean;
|
8
8
|
isActive: boolean;
|
9
9
|
label: string;
|
10
|
+
icon?: JSX.Element;
|
10
11
|
};
|
11
12
|
|
12
|
-
const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }: DropdownButtonProps) => {
|
13
|
+
const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label, icon }: DropdownButtonProps) => {
|
13
14
|
return (
|
14
15
|
<button
|
15
16
|
aria-label={label}
|
@@ -20,8 +21,11 @@ const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }
|
|
20
21
|
disabled={isDisabled}
|
21
22
|
className={`btn dropdown-button ${isActive ? 'is-active' : ''}`}
|
22
23
|
>
|
23
|
-
<
|
24
|
-
|
24
|
+
<div className="dropdown-button-label">
|
25
|
+
{icon && icon}
|
26
|
+
{children || label}
|
27
|
+
</div>
|
28
|
+
{isActive && <CheckRoundedIcon className="dropdown-button-icon" />}
|
25
29
|
</button>
|
26
30
|
);
|
27
31
|
};
|
package/src/ui/_typography.scss
CHANGED
@@ -44,3 +44,29 @@ h6 {
|
|
44
44
|
letter-spacing: -0.2px;
|
45
45
|
line-height: 1.25rem;
|
46
46
|
}
|
47
|
+
|
48
|
+
.code-block,
|
49
|
+
.preformatted {
|
50
|
+
display: flex;
|
51
|
+
|
52
|
+
.material-symbols-rounded {
|
53
|
+
@apply text-gray-700 text-xlg;
|
54
|
+
pointer-events: none;
|
55
|
+
margin-right: 0.375rem;
|
56
|
+
}
|
57
|
+
|
58
|
+
.block-divider {
|
59
|
+
@apply bg-gray-200;
|
60
|
+
width: 4px;
|
61
|
+
border-radius: 0.75rem;
|
62
|
+
margin-right: 0.625rem;
|
63
|
+
}
|
64
|
+
|
65
|
+
code,
|
66
|
+
pre {
|
67
|
+
@apply text-gray-700;
|
68
|
+
font-size: 0.75rem;
|
69
|
+
padding-top: 0.75rem;
|
70
|
+
padding-bottom: 0.75rem;
|
71
|
+
}
|
72
|
+
}
|
@@ -74,6 +74,43 @@ describe('remirrorNodeToSquizNode', () => {
|
|
74
74
|
expect(result).toEqual(expected);
|
75
75
|
});
|
76
76
|
|
77
|
+
it('should handle code block formatting', async () => {
|
78
|
+
const content: RemirrorJSON = {
|
79
|
+
type: 'doc',
|
80
|
+
content: [
|
81
|
+
{
|
82
|
+
type: 'codeBlock',
|
83
|
+
attrs: {
|
84
|
+
language: 'js',
|
85
|
+
wrap: true,
|
86
|
+
},
|
87
|
+
content: [
|
88
|
+
{
|
89
|
+
type: 'text',
|
90
|
+
text: 'Hello world',
|
91
|
+
},
|
92
|
+
],
|
93
|
+
},
|
94
|
+
],
|
95
|
+
};
|
96
|
+
|
97
|
+
const { editor } = await renderWithEditor(null, { content });
|
98
|
+
|
99
|
+
const expected: FormattedText = [
|
100
|
+
{
|
101
|
+
type: 'tag',
|
102
|
+
tag: 'code',
|
103
|
+
children: [{ type: 'text', value: 'Hello world' }],
|
104
|
+
attributes: {
|
105
|
+
language: 'js',
|
106
|
+
},
|
107
|
+
},
|
108
|
+
];
|
109
|
+
|
110
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
111
|
+
expect(result).toEqual(expected);
|
112
|
+
});
|
113
|
+
|
77
114
|
it('should handle images', async () => {
|
78
115
|
const content: RemirrorJSON = {
|
79
116
|
type: 'doc',
|
@@ -16,10 +16,14 @@ type FormattedNodeFontProperties = FormattedTextModels.v1.FormattedNodeFontPrope
|
|
16
16
|
type FormattedNodeWithChildren = Extract<FormattedNode, { children: FormattedNode[] }>;
|
17
17
|
|
18
18
|
export const resolveNodeTag = (node: ProsemirrorNode): string => {
|
19
|
-
if (node.type.name ===
|
19
|
+
if (node.type.name === NodeName.Text) {
|
20
20
|
return 'span';
|
21
21
|
}
|
22
22
|
|
23
|
+
if (node.type.name === NodeName.CodeBlock) {
|
24
|
+
return 'code';
|
25
|
+
}
|
26
|
+
|
23
27
|
if (node.type.spec?.toDOM) {
|
24
28
|
const domNode = node.type.spec.toDOM(node);
|
25
29
|
|
@@ -92,9 +96,13 @@ const transformFragment = (fragment: Fragment): FormattedText => {
|
|
92
96
|
};
|
93
97
|
|
94
98
|
const transformNode = (node: ProsemirrorNode): FormattedNode => {
|
95
|
-
const attributes = node.type.name === NodeName.Image ? transformAttributes(node.attrs) : undefined;
|
96
99
|
const formattingOptions = undefinedIfEmpty(resolveFormattingOptions(node));
|
97
100
|
const font = undefinedIfEmpty(resolveFontOptions(node));
|
101
|
+
const attributes =
|
102
|
+
node.type.name === NodeName.Image || node.type.name === NodeName.CodeBlock
|
103
|
+
? transformAttributes(node.attrs)
|
104
|
+
: undefined;
|
105
|
+
|
98
106
|
let transformedNode: FormattedNode = { type: 'text', value: node.text || '' };
|
99
107
|
|
100
108
|
// Squiz "text" nodes can't have formatting/font options but Remirror "text" nodes can.
|
@@ -139,10 +139,10 @@ describe('squizNodeToRemirrorNode', () => {
|
|
139
139
|
},
|
140
140
|
],
|
141
141
|
type: 'tag',
|
142
|
-
tag: '
|
142
|
+
tag: 'video',
|
143
143
|
},
|
144
144
|
],
|
145
|
-
'Unsupported node type provided: tag (tag:
|
145
|
+
'Unsupported node type provided: tag (tag: video)',
|
146
146
|
],
|
147
147
|
[
|
148
148
|
'Unsupported node type',
|
@@ -192,6 +192,43 @@ describe('squizNodeToRemirrorNode', () => {
|
|
192
192
|
expect(result).toEqual(expected);
|
193
193
|
});
|
194
194
|
|
195
|
+
it('should handle code block text', () => {
|
196
|
+
const squizComponentJSON: FormattedText = [
|
197
|
+
{
|
198
|
+
children: [
|
199
|
+
{
|
200
|
+
type: 'text',
|
201
|
+
value: 'Hello world!',
|
202
|
+
},
|
203
|
+
],
|
204
|
+
type: 'tag',
|
205
|
+
tag: 'code',
|
206
|
+
},
|
207
|
+
];
|
208
|
+
|
209
|
+
const expected: RemirrorJSON = {
|
210
|
+
content: [
|
211
|
+
{
|
212
|
+
type: 'codeBlock',
|
213
|
+
attrs: {
|
214
|
+
language: 'markup',
|
215
|
+
wrap: true,
|
216
|
+
},
|
217
|
+
content: [
|
218
|
+
{
|
219
|
+
type: 'text',
|
220
|
+
text: 'Hello world!',
|
221
|
+
},
|
222
|
+
],
|
223
|
+
},
|
224
|
+
],
|
225
|
+
type: 'doc',
|
226
|
+
};
|
227
|
+
|
228
|
+
const result = squizNodeToRemirrorNode(squizComponentJSON);
|
229
|
+
expect(result).toEqual(expected);
|
230
|
+
});
|
231
|
+
|
195
232
|
it('should handle images', () => {
|
196
233
|
const squizComponentJSON: FormattedText = [
|
197
234
|
{
|
@@ -26,6 +26,7 @@ const getNodeType = (node: FormattedNodes): string => {
|
|
26
26
|
p: 'paragraph',
|
27
27
|
a: NodeName.Text,
|
28
28
|
span: NodeName.Text,
|
29
|
+
code: NodeName.CodeBlock,
|
29
30
|
};
|
30
31
|
|
31
32
|
if (typeMap[node.type]) {
|
@@ -53,6 +54,11 @@ const getNodeAttributes = (node: FormattedNodes): Attrs => {
|
|
53
54
|
src: node.attributes?.src,
|
54
55
|
title: node.attributes?.title,
|
55
56
|
};
|
57
|
+
} else if (node.type === 'tag' && node.tag === 'code') {
|
58
|
+
return {
|
59
|
+
language: node.attributes?.language || 'markup',
|
60
|
+
wrap: node.attributes?.wrap || true,
|
61
|
+
};
|
56
62
|
} else if (node.type === 'matrix-image') {
|
57
63
|
return {
|
58
64
|
matrixAssetId: node.matrixAssetId,
|
@@ -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', 'image']);
|
18
|
+
expect(otherNodeNames).toEqual(['assetImage', 'doc', 'text', 'codeBlock', 'image']);
|
19
19
|
});
|
20
20
|
});
|
package/tailwind.config.cjs
CHANGED
@@ -27,9 +27,6 @@ module.exports = {
|
|
27
27
|
'heading-3': ['1.125rem', '1.375rem'],
|
28
28
|
'heading-4': ['1rem', '1.25rem'],
|
29
29
|
},
|
30
|
-
fontFamily: {
|
31
|
-
base: 'Open Sans, Arial, sans-serif',
|
32
|
-
},
|
33
30
|
boxShadow: {
|
34
31
|
outline: '0 0 0 1px rgba(0,0,0,0.10)',
|
35
32
|
sm: '0 0 0 1px rgba(0,0,0,0.04), 0 1px 4px 2px rgba(0,0,0,0.08)',
|
@@ -75,6 +72,10 @@ module.exports = {
|
|
75
72
|
300: '#0774d2',
|
76
73
|
400: '#044985',
|
77
74
|
},
|
75
|
+
teal: {
|
76
|
+
100: '#E6F4F6',
|
77
|
+
400: '#024752',
|
78
|
+
},
|
78
79
|
red: {
|
79
80
|
300: '#d72321',
|
80
81
|
},
|