@squiz/formatted-text-editor 1.34.1-alpha.0 → 1.34.1-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/lib/EditorToolbar/FloatingToolbar.js +7 -1
  2. package/lib/EditorToolbar/Toolbar.js +4 -2
  3. package/lib/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.d.ts +2 -0
  4. package/lib/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.js +56 -0
  5. package/lib/EditorToolbar/Tools/Image/ImageButton.js +1 -1
  6. package/lib/EditorToolbar/Tools/Link/LinkButton.js +1 -1
  7. package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.js +2 -2
  8. package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.d.ts +2 -0
  9. package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.js +22 -0
  10. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +3 -2
  11. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +8 -2
  12. package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.d.ts +5 -0
  13. package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.js +63 -0
  14. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +5 -0
  15. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +30 -0
  16. package/lib/Extensions/Extensions.d.ts +1 -0
  17. package/lib/Extensions/Extensions.js +5 -0
  18. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +2 -0
  19. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +23 -0
  20. package/lib/index.css +50 -9
  21. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.d.ts +2 -1
  22. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +6 -4
  23. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +7 -2
  24. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +7 -0
  25. package/lib/utils/getMarkNamesByGroup.d.ts +2 -0
  26. package/lib/utils/getMarkNamesByGroup.js +9 -0
  27. package/lib/utils/getNodeNamesByGroup.d.ts +2 -0
  28. package/lib/utils/getNodeNamesByGroup.js +9 -0
  29. package/package.json +4 -4
  30. package/postcss.config.js +1 -1
  31. package/src/EditorToolbar/FloatingToolbar.tsx +10 -5
  32. package/src/EditorToolbar/Toolbar.tsx +3 -1
  33. package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.spec.tsx +34 -0
  34. package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.tsx +45 -0
  35. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +3 -2
  36. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +3 -2
  37. package/src/EditorToolbar/Tools/TextAlign/TextAlignButtons.tsx +2 -2
  38. package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.spec.tsx +47 -0
  39. package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.tsx +32 -0
  40. package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.spec.tsx +2 -2
  41. package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.spec.tsx +1 -1
  42. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.spec.tsx +2 -2
  43. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.tsx +3 -1
  44. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +12 -2
  45. package/src/EditorToolbar/_floating-toolbar.scss +1 -1
  46. package/src/EditorToolbar/_toolbar.scss +2 -2
  47. package/src/Extensions/ClearFormattingExtension/ClearFormattingExtension.ts +57 -0
  48. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +34 -0
  49. package/src/Extensions/Extensions.ts +5 -0
  50. package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +4 -2
  51. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +31 -0
  52. package/src/index.scss +3 -0
  53. package/src/ui/ToolbarDropdownButton/ToolbarDropdownButton.tsx +8 -4
  54. package/src/ui/ToolbarDropdownButton/_toolbar-dropdown-button.scss +11 -0
  55. package/src/ui/_typography.scss +26 -0
  56. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +37 -0
  57. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -2
  58. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +39 -2
  59. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +6 -0
  60. package/src/utils/getMarkNamesByGroup.spec.ts +20 -0
  61. package/src/utils/getMarkNamesByGroup.ts +7 -0
  62. package/src/utils/getNodeNamesByGroup.spec.ts +20 -0
  63. package/src/utils/getNodeNamesByGroup.ts +7 -0
  64. 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.0",
3
+ "version": "1.34.1-alpha.10",
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.0",
24
- "@squiz/resource-browser": "1.34.1-alpha.0",
23
+ "@squiz/dx-json-schema-lib": "1.34.1-alpha.10",
24
+ "@squiz/resource-browser": "1.34.1-alpha.10",
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": "06cf1fb6cbd27b5c324b63ad7e4bf542faf99bfd"
78
+ "gitHead": "7cc7ba46d059e70fa58da3cb5f8221ad0e8485a7"
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: ['.squiz-fte-scope__floating-popover'],
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
- <VerticalDivider key="link-divider" className="editor-divider" />,
43
- <LinkButton key="add-link" inPopover={true} />,
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 className="editor-divider" />
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 className="editor-divider" />
11
+ <VerticalDivider />
12
12
  <LeftAlignButton />
13
13
  <CenterAlignButton />
14
14
  <RightAlignButton />
15
15
  <JustifyAlignButton />
16
- <VerticalDivider className="editor-divider" />
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="CheckIcon"]')).toBeFalsy();
39
+ expect(headingButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
40
40
 
41
41
  fireEvent.click(headingButton);
42
- expect(headingButton.querySelector('svg[data-testid="CheckIcon"]')).toBeTruthy();
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="CheckIcon"]')).toBeTruthy();
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="CheckIcon"]')).toBeFalsy();
30
+ expect(preformattedButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
31
31
 
32
32
  fireEvent.click(preformattedButton);
33
- expect(preformattedButton.querySelector('svg[data-testid="CheckIcon"]')).toBeTruthy();
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
- <pre>Preformatted</pre>
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 className="editor-divider" />
49
+ <VerticalDivider />
40
50
  </>
41
51
  );
42
52
  };
@@ -3,7 +3,7 @@
3
3
  @extend .editor-toolbar;
4
4
  @apply bg-white border-gray-200 p-1 shadow rounded-md border flex;
5
5
 
6
- .editor-divider {
6
+ .MuiDivider-root {
7
7
  @apply my-0;
8
8
  }
9
9
  }
@@ -4,11 +4,11 @@
4
4
  display: flex;
5
5
  justify-items: center;
6
6
 
7
- > *:not(:first-child, .editor-divider) {
7
+ > *:not(:first-child, .MuiDivider-root) {
8
8
  margin: 0 0 0 2px;
9
9
  }
10
10
 
11
- .editor-divider {
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('AssetLinkExtension', () => {
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('<pre style="">This is some preformatted text</pre>');
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';