@squiz/formatted-text-editor 1.34.1-alpha.1 → 1.34.1-alpha.11
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/FloatingToolbar.js +7 -1
- package/lib/EditorToolbar/Toolbar.js +4 -2
- package/lib/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.d.ts +2 -0
- package/lib/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.js +56 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +1 -1
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +1 -1
- package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.js +2 -2
- 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 +8 -2
- package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.d.ts +5 -0
- package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.js +63 -0
- 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 +5 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +2 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +23 -0
- package/lib/index.css +50 -9
- 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/lib/utils/getMarkNamesByGroup.d.ts +2 -0
- package/lib/utils/getMarkNamesByGroup.js +9 -0
- package/lib/utils/getNodeNamesByGroup.d.ts +2 -0
- package/lib/utils/getNodeNamesByGroup.js +9 -0
- package/package.json +4 -4
- package/postcss.config.js +1 -1
- package/src/EditorToolbar/FloatingToolbar.tsx +10 -5
- package/src/EditorToolbar/Toolbar.tsx +3 -1
- package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.spec.tsx +34 -0
- package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.tsx +45 -0
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +3 -2
- package/src/EditorToolbar/Tools/TextAlign/TextAlignButtons.tsx +2 -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 +12 -2
- package/src/EditorToolbar/_floating-toolbar.scss +1 -1
- package/src/EditorToolbar/_toolbar.scss +2 -2
- package/src/Extensions/ClearFormattingExtension/ClearFormattingExtension.ts +57 -0
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +34 -0
- package/src/Extensions/Extensions.ts +5 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +4 -2
- 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/getMarkNamesByGroup.spec.ts +20 -0
- package/src/utils/getMarkNamesByGroup.ts +7 -0
- package/src/utils/getNodeNamesByGroup.spec.ts +20 -0
- package/src/utils/getNodeNamesByGroup.ts +7 -0
- package/tailwind.config.cjs +4 -3
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.11",
|
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.11",
|
24
|
+
"@squiz/resource-browser": "1.34.1-alpha.11",
|
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": "03a5268d9b5727fbe68848b3c2222f5c5dfb8c33"
|
79
79
|
}
|
package/postcss.config.js
CHANGED
@@ -5,7 +5,7 @@ module.exports = {
|
|
5
5
|
require('postcss-nested'),
|
6
6
|
require('postcss-prefix-selector')({
|
7
7
|
prefix: '.squiz-fte-scope',
|
8
|
-
exclude: [
|
8
|
+
exclude: [/\.squiz-fte-scope__floating-popover/],
|
9
9
|
includeFiles: ['./src/index.scss'],
|
10
10
|
}),
|
11
11
|
],
|
@@ -5,17 +5,20 @@ import BoldButton from './Tools/Bold/BoldButton';
|
|
5
5
|
import { useExtensionNames } from '../hooks';
|
6
6
|
import RemoveLinkButton from './Tools/Link/RemoveLinkButton';
|
7
7
|
import LinkButton from './Tools/Link/LinkButton';
|
8
|
-
import { FloatingToolbar as RemirrorFloatingToolbar, useActive, usePositioner } from '@remirror/react';
|
8
|
+
import { FloatingToolbar as RemirrorFloatingToolbar, useActive, usePositioner, useCommands } from '@remirror/react';
|
9
9
|
import { VerticalDivider } from '@remirror/react-components';
|
10
10
|
import { createToolbarPositioner, ToolbarPositionerRange } from '../utils/createToolbarPositioner';
|
11
11
|
import ImageButton from './Tools/Image/ImageButton';
|
12
12
|
import { MarkName } from '../Extensions/Extensions';
|
13
13
|
import { ImageExtension } from '../Extensions/ImageExtension/ImageExtension';
|
14
|
+
import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton';
|
15
|
+
import { ClearFormattingExtension } from '../Extensions/ClearFormattingExtension/ClearFormattingExtension';
|
14
16
|
|
15
17
|
export const FloatingToolbar = () => {
|
16
18
|
const watchedMarks = [MarkName.Link, MarkName.AssetLink];
|
17
19
|
const extensionNames = useExtensionNames();
|
18
20
|
const positioner = useMemo(() => createToolbarPositioner({ types: watchedMarks }), []);
|
21
|
+
const { clearFormatting } = useCommands<ClearFormattingExtension>();
|
19
22
|
const active = useActive<ImageExtension>();
|
20
23
|
const {
|
21
24
|
data: { marks },
|
@@ -38,10 +41,12 @@ export const FloatingToolbar = () => {
|
|
38
41
|
];
|
39
42
|
} else if (!marks?.[MarkName.Link].isActive && !marks?.[MarkName.AssetLink].isActive) {
|
40
43
|
// if none of the selected text is a link show the option to create a link.
|
41
|
-
buttons.push(
|
42
|
-
|
43
|
-
|
44
|
-
|
44
|
+
buttons.push(<VerticalDivider key="link-divider" />, <LinkButton key="add-link" inPopover={true} />);
|
45
|
+
}
|
46
|
+
|
47
|
+
// Clear formatting will always be the last button in the toolbar
|
48
|
+
if (extensionNames.clearFormatting && clearFormatting.enabled()) {
|
49
|
+
buttons.push(<ClearFormattingButton key="clearFormatting" />);
|
45
50
|
}
|
46
51
|
|
47
52
|
return (
|
@@ -11,6 +11,7 @@ import { useExtensionNames } from '../hooks';
|
|
11
11
|
import LinkButton from './Tools/Link/LinkButton';
|
12
12
|
import ImageButton from './Tools/Image/ImageButton';
|
13
13
|
import RemoveLinkButton from './Tools/Link/RemoveLinkButton';
|
14
|
+
import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton';
|
14
15
|
|
15
16
|
export const Toolbar = () => {
|
16
17
|
const extensionNames = useExtensionNames();
|
@@ -21,7 +22,7 @@ export const Toolbar = () => {
|
|
21
22
|
<>
|
22
23
|
<UndoButton />
|
23
24
|
<RedoButton />
|
24
|
-
<VerticalDivider
|
25
|
+
<VerticalDivider />
|
25
26
|
</>
|
26
27
|
)}
|
27
28
|
{extensionNames.heading && extensionNames.paragraph && extensionNames.preformatted && <TextTypeDropdown />}
|
@@ -36,6 +37,7 @@ export const Toolbar = () => {
|
|
36
37
|
</>
|
37
38
|
)}
|
38
39
|
{extensionNames.image && <ImageButton />}
|
40
|
+
{extensionNames.clearFormatting && <ClearFormattingButton />}
|
39
41
|
</RemirrorToolbar>
|
40
42
|
);
|
41
43
|
};
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import '@testing-library/jest-dom';
|
3
|
+
import { screen, fireEvent } from '@testing-library/react';
|
4
|
+
import { renderWithEditor } from '../../../../tests';
|
5
|
+
import ClearFormattingButton from './ClearFormattingButton';
|
6
|
+
|
7
|
+
describe('Clear formatting button', () => {
|
8
|
+
it('Renders the clear formatting button', async () => {
|
9
|
+
await renderWithEditor(<ClearFormattingButton />, { content: 'Some nonsense content here' });
|
10
|
+
expect(screen.getByRole('button', { name: 'Clear all formatting (cmd+\\)' })).toBeInTheDocument();
|
11
|
+
});
|
12
|
+
|
13
|
+
it('Clears the formatting from editor content after clicking button', async () => {
|
14
|
+
const { getHtmlContent } = await renderWithEditor(<ClearFormattingButton />, {
|
15
|
+
content: '<p>Hello <strong>Mr Bean</strong></p>',
|
16
|
+
});
|
17
|
+
|
18
|
+
const clearFormatting = screen.getByRole('button', { name: 'Clear all formatting (cmd+\\)' });
|
19
|
+
fireEvent.click(clearFormatting);
|
20
|
+
|
21
|
+
expect(getHtmlContent()).toBe('<p style="">Hello Mr Bean</p>');
|
22
|
+
});
|
23
|
+
|
24
|
+
it('Clears the formatting from editor content when shortcut is pressed', async () => {
|
25
|
+
const { getHtmlContent } = await renderWithEditor(<ClearFormattingButton />, {
|
26
|
+
content: '<p>Hello <strong>Mr Bean</strong></p>',
|
27
|
+
});
|
28
|
+
|
29
|
+
const editor = screen.getByRole('textbox'); // Assuming the editor is an input field or textarea
|
30
|
+
fireEvent.keyDown(editor, { key: '\\', code: 'Backslash', ctrlKey: true });
|
31
|
+
|
32
|
+
expect(getHtmlContent()).toBe('<p style="">Hello Mr Bean</p>');
|
33
|
+
});
|
34
|
+
});
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import React, { useCallback } from 'react';
|
2
|
+
import { useCommands, useEditorState, useKeymap } from '@remirror/react';
|
3
|
+
import { VerticalDivider } from '@remirror/react-components';
|
4
|
+
import FormatClearRoundedIcon from '@mui/icons-material/FormatClearRounded';
|
5
|
+
import { ClearFormattingExtension } from '../../../Extensions/ClearFormattingExtension/ClearFormattingExtension';
|
6
|
+
import Button from '../../../ui/Button/Button';
|
7
|
+
|
8
|
+
const ClearFormattingButton = () => {
|
9
|
+
const { clearFormatting } = useCommands<ClearFormattingExtension>();
|
10
|
+
const { selection } = useEditorState();
|
11
|
+
|
12
|
+
// Checks wether we have specific content selected or not
|
13
|
+
const contentSelected = !selection.empty;
|
14
|
+
|
15
|
+
const handleSelect = () => {
|
16
|
+
if (clearFormatting.enabled()) {
|
17
|
+
clearFormatting();
|
18
|
+
}
|
19
|
+
};
|
20
|
+
|
21
|
+
const handleShortcut = useCallback(() => {
|
22
|
+
handleSelect();
|
23
|
+
// Prevent other key handlers being run
|
24
|
+
return true;
|
25
|
+
}, []);
|
26
|
+
|
27
|
+
// When Ctrl+\ is pressed clear formatting, only registered in the toolbar button instance to avoid the key press
|
28
|
+
// being double handled.
|
29
|
+
useKeymap('Mod-\\', handleShortcut);
|
30
|
+
|
31
|
+
return (
|
32
|
+
<>
|
33
|
+
<VerticalDivider />
|
34
|
+
<Button
|
35
|
+
handleOnClick={handleSelect}
|
36
|
+
isDisabled={false}
|
37
|
+
isActive={false}
|
38
|
+
icon={<FormatClearRoundedIcon />}
|
39
|
+
label={`${contentSelected ? 'Clear formatting from selection' : 'Clear all formatting'} (cmd+\\)`}
|
40
|
+
/>
|
41
|
+
</>
|
42
|
+
);
|
43
|
+
};
|
44
|
+
|
45
|
+
export default ClearFormattingButton;
|
@@ -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);
|
@@ -8,12 +8,12 @@ import { VerticalDivider } from '@remirror/react-components';
|
|
8
8
|
const TextAlignButtons = () => {
|
9
9
|
return (
|
10
10
|
<>
|
11
|
-
<VerticalDivider
|
11
|
+
<VerticalDivider />
|
12
12
|
<LeftAlignButton />
|
13
13
|
<CenterAlignButton />
|
14
14
|
<RightAlignButton />
|
15
15
|
<JustifyAlignButton />
|
16
|
-
<VerticalDivider
|
16
|
+
<VerticalDivider />
|
17
17
|
</>
|
18
18
|
);
|
19
19
|
};
|
@@ -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,8 +44,9 @@ const TextTypeDropdown = () => {
|
|
35
44
|
<HeadingButton level={5} />
|
36
45
|
<HeadingButton level={6} />
|
37
46
|
<PreformattedButton />
|
47
|
+
<CodeBlockButton />
|
38
48
|
</ToolbarDropdown>
|
39
|
-
<VerticalDivider
|
49
|
+
<VerticalDivider />
|
40
50
|
</>
|
41
51
|
);
|
42
52
|
};
|
@@ -4,11 +4,11 @@
|
|
4
4
|
display: flex;
|
5
5
|
justify-items: center;
|
6
6
|
|
7
|
-
> *:not(:first-child, .
|
7
|
+
> *:not(:first-child, .MuiDivider-root) {
|
8
8
|
margin: 0 0 0 2px;
|
9
9
|
}
|
10
10
|
|
11
|
-
.
|
11
|
+
.MuiDivider-root {
|
12
12
|
@apply -my-1 mx-1 border;
|
13
13
|
margin-right: 2px;
|
14
14
|
height: auto;
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import { command, extension, PlainExtension, CommandFunction, Mark, ExtensionTag } from '@remirror/core';
|
2
|
+
import { getNodeNamesByGroup } from '../../utils/getNodeNamesByGroup';
|
3
|
+
import { getMarkNamesByGroup } from '../../utils/getMarkNamesByGroup';
|
4
|
+
|
5
|
+
@extension({})
|
6
|
+
export class ClearFormattingExtension extends PlainExtension {
|
7
|
+
get name() {
|
8
|
+
return 'clearFormatting' as const;
|
9
|
+
}
|
10
|
+
|
11
|
+
@command()
|
12
|
+
clearFormatting(): CommandFunction {
|
13
|
+
return ({ dispatch, tr, state }) => {
|
14
|
+
const { empty, ranges } = state.selection;
|
15
|
+
const schema = state.schema;
|
16
|
+
|
17
|
+
const formattingNodes = getNodeNamesByGroup(schema, ExtensionTag.FormattingNode);
|
18
|
+
const formattingMarks = getMarkNamesByGroup(schema, ExtensionTag.FormattingMark);
|
19
|
+
let isChanged = false;
|
20
|
+
|
21
|
+
ranges.forEach(({ $from, $to }) => {
|
22
|
+
// Check if there is a selection or not, if no selection use the doc content size as the range
|
23
|
+
state.doc.nodesBetween(empty ? 0 : $from.pos, empty ? state.doc.content.size : $to.pos, (node, pos) => {
|
24
|
+
// Clear marks (bold, italic, etc)
|
25
|
+
node.marks.forEach((mark: Mark) => {
|
26
|
+
if (formattingMarks.includes(mark.type.name)) {
|
27
|
+
tr.removeMark(pos, pos + node.nodeSize, mark);
|
28
|
+
isChanged = true;
|
29
|
+
}
|
30
|
+
});
|
31
|
+
|
32
|
+
// Leave non-foramtting nodes as-is
|
33
|
+
if (!formattingNodes.includes(node.type.name)) {
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
|
37
|
+
// Clear node attributes & set to paragraph by default
|
38
|
+
if (node.type.name === schema.nodes.paragraph.name) {
|
39
|
+
const { nodeTextAlignment } = node.attrs;
|
40
|
+
|
41
|
+
if (nodeTextAlignment && nodeTextAlignment !== 'left') {
|
42
|
+
tr.setNodeAttribute(pos, 'nodeTextAlignment', null);
|
43
|
+
isChanged = true;
|
44
|
+
}
|
45
|
+
} else {
|
46
|
+
tr.setNodeMarkup(pos, schema.nodes.paragraph, null, node.marks);
|
47
|
+
isChanged = true;
|
48
|
+
}
|
49
|
+
});
|
50
|
+
});
|
51
|
+
|
52
|
+
dispatch?.(tr);
|
53
|
+
|
54
|
+
return isChanged;
|
55
|
+
};
|
56
|
+
}
|
57
|
+
}
|
@@ -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,9 +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';
|
19
|
+
import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
|
18
20
|
|
19
21
|
export enum NodeName {
|
20
22
|
Image = 'image',
|
23
|
+
CodeBlock = 'codeBlock',
|
21
24
|
AssetImage = 'assetImage',
|
22
25
|
Text = 'text',
|
23
26
|
}
|
@@ -37,6 +40,7 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
37
40
|
new NodeFormattingExtension({ indents: [] }),
|
38
41
|
new ParagraphExtension(),
|
39
42
|
new PreformattedExtension(),
|
43
|
+
new ExtendedCodeBlockExtension({ defaultWrap: true }),
|
40
44
|
new UnderlineExtension(),
|
41
45
|
new HistoryExtension(),
|
42
46
|
new ImageExtension(),
|
@@ -48,6 +52,7 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
48
52
|
new AssetLinkExtension({
|
49
53
|
matrixDomain: context.matrix.matrixDomain,
|
50
54
|
}),
|
55
|
+
new ClearFormattingExtension(),
|
51
56
|
];
|
52
57
|
};
|
53
58
|
};
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { renderWithEditor } from '../../../tests';
|
2
2
|
|
3
|
-
describe('
|
3
|
+
describe('PreformattedExtension', () => {
|
4
4
|
it('Parses HTML content with preformatted text', async () => {
|
5
5
|
const { getJsonContent } = await renderWithEditor(null, {
|
6
6
|
content: `<pre>This is some preformatted text</pre>`,
|
@@ -36,6 +36,8 @@ describe('AssetLinkExtension', () => {
|
|
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';
|