@squiz/formatted-text-editor 1.34.1-alpha.4 → 1.34.1-alpha.6

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 (31) 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/TextAlign/TextAlignButtons.js +2 -2
  6. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +1 -1
  7. package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.d.ts +5 -0
  8. package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.js +63 -0
  9. package/lib/Extensions/Extensions.js +2 -0
  10. package/lib/index.css +9 -9
  11. package/lib/utils/getMarkNamesByGroup.d.ts +2 -0
  12. package/lib/utils/getMarkNamesByGroup.js +9 -0
  13. package/lib/utils/getNodeNamesByGroup.d.ts +2 -0
  14. package/lib/utils/getNodeNamesByGroup.js +9 -0
  15. package/package.json +4 -4
  16. package/postcss.config.js +1 -1
  17. package/src/EditorToolbar/FloatingToolbar.tsx +10 -5
  18. package/src/EditorToolbar/Toolbar.tsx +3 -1
  19. package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.spec.tsx +34 -0
  20. package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.tsx +45 -0
  21. package/src/EditorToolbar/Tools/TextAlign/TextAlignButtons.tsx +2 -2
  22. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +1 -1
  23. package/src/EditorToolbar/_floating-toolbar.scss +1 -1
  24. package/src/EditorToolbar/_toolbar.scss +2 -2
  25. package/src/Extensions/ClearFormattingExtension/ClearFormattingExtension.ts +57 -0
  26. package/src/Extensions/Extensions.ts +2 -0
  27. package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +1 -1
  28. package/src/utils/getMarkNamesByGroup.spec.ts +20 -0
  29. package/src/utils/getMarkNamesByGroup.ts +7 -0
  30. package/src/utils/getNodeNamesByGroup.spec.ts +20 -0
  31. package/src/utils/getNodeNamesByGroup.ts +7 -0
@@ -39,10 +39,12 @@ const react_components_1 = require("@remirror/react-components");
39
39
  const createToolbarPositioner_1 = require("../utils/createToolbarPositioner");
40
40
  const ImageButton_1 = __importDefault(require("./Tools/Image/ImageButton"));
41
41
  const Extensions_1 = require("../Extensions/Extensions");
42
+ const ClearFormattingButton_1 = __importDefault(require("./Tools/ClearFormatting/ClearFormattingButton"));
42
43
  const FloatingToolbar = () => {
43
44
  const watchedMarks = [Extensions_1.MarkName.Link, Extensions_1.MarkName.AssetLink];
44
45
  const extensionNames = (0, hooks_1.useExtensionNames)();
45
46
  const positioner = (0, react_1.useMemo)(() => (0, createToolbarPositioner_1.createToolbarPositioner)({ types: watchedMarks }), []);
47
+ const { clearFormatting } = (0, react_2.useCommands)();
46
48
  const active = (0, react_2.useActive)();
47
49
  const { data: { marks }, } = (0, react_2.usePositioner)(positioner, []);
48
50
  let buttons = [
@@ -63,7 +65,11 @@ const FloatingToolbar = () => {
63
65
  }
64
66
  else if (!marks?.[Extensions_1.MarkName.Link].isActive && !marks?.[Extensions_1.MarkName.AssetLink].isActive) {
65
67
  // if none of the selected text is a link show the option to create a link.
66
- buttons.push(react_1.default.createElement(react_components_1.VerticalDivider, { key: "link-divider", className: "editor-divider" }), react_1.default.createElement(LinkButton_1.default, { key: "add-link", inPopover: true }));
68
+ buttons.push(react_1.default.createElement(react_components_1.VerticalDivider, { key: "link-divider" }), react_1.default.createElement(LinkButton_1.default, { key: "add-link", inPopover: true }));
69
+ }
70
+ // Clear formatting will always be the last button in the toolbar
71
+ if (extensionNames.clearFormatting && clearFormatting.enabled()) {
72
+ buttons.push(react_1.default.createElement(ClearFormattingButton_1.default, { key: "clearFormatting" }));
67
73
  }
68
74
  return (react_1.default.createElement(react_2.FloatingToolbar, { className: "squiz-fte-scope squiz-fte-scope__floating-popover", positioner: positioner }, buttons));
69
75
  };
@@ -17,13 +17,14 @@ const hooks_1 = require("../hooks");
17
17
  const LinkButton_1 = __importDefault(require("./Tools/Link/LinkButton"));
18
18
  const ImageButton_1 = __importDefault(require("./Tools/Image/ImageButton"));
19
19
  const RemoveLinkButton_1 = __importDefault(require("./Tools/Link/RemoveLinkButton"));
20
+ const ClearFormattingButton_1 = __importDefault(require("./Tools/ClearFormatting/ClearFormattingButton"));
20
21
  const Toolbar = () => {
21
22
  const extensionNames = (0, hooks_1.useExtensionNames)();
22
23
  return (react_1.default.createElement(react_components_1.Toolbar, { className: "remirror-toolbar editor-toolbar" },
23
24
  extensionNames.history && (react_1.default.createElement(react_1.default.Fragment, null,
24
25
  react_1.default.createElement(UndoButton_1.default, null),
25
26
  react_1.default.createElement(RedoButton_1.default, null),
26
- react_1.default.createElement(react_components_1.VerticalDivider, { className: "editor-divider" }))),
27
+ react_1.default.createElement(react_components_1.VerticalDivider, null))),
27
28
  extensionNames.heading && extensionNames.paragraph && extensionNames.preformatted && react_1.default.createElement(TextTypeDropdown_1.default, null),
28
29
  extensionNames.bold && react_1.default.createElement(BoldButton_1.default, null),
29
30
  extensionNames.italic && react_1.default.createElement(ItalicButton_1.default, null),
@@ -32,6 +33,7 @@ const Toolbar = () => {
32
33
  extensionNames.link && (react_1.default.createElement(react_1.default.Fragment, null,
33
34
  react_1.default.createElement(LinkButton_1.default, null),
34
35
  react_1.default.createElement(RemoveLinkButton_1.default, null))),
35
- extensionNames.image && react_1.default.createElement(ImageButton_1.default, null)));
36
+ extensionNames.image && react_1.default.createElement(ImageButton_1.default, null),
37
+ extensionNames.clearFormatting && react_1.default.createElement(ClearFormattingButton_1.default, null)));
36
38
  };
37
39
  exports.Toolbar = Toolbar;
@@ -0,0 +1,2 @@
1
+ declare const ClearFormattingButton: () => JSX.Element;
2
+ export default ClearFormattingButton;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const react_1 = __importStar(require("react"));
30
+ const react_2 = require("@remirror/react");
31
+ const react_components_1 = require("@remirror/react-components");
32
+ const FormatClearRounded_1 = __importDefault(require("@mui/icons-material/FormatClearRounded"));
33
+ const Button_1 = __importDefault(require("../../../ui/Button/Button"));
34
+ const ClearFormattingButton = () => {
35
+ const { clearFormatting } = (0, react_2.useCommands)();
36
+ const { selection } = (0, react_2.useEditorState)();
37
+ // Checks wether we have specific content selected or not
38
+ const contentSelected = !selection.empty;
39
+ const handleSelect = () => {
40
+ if (clearFormatting.enabled()) {
41
+ clearFormatting();
42
+ }
43
+ };
44
+ const handleShortcut = (0, react_1.useCallback)(() => {
45
+ handleSelect();
46
+ // Prevent other key handlers being run
47
+ return true;
48
+ }, []);
49
+ // When Ctrl+\ is pressed clear formatting, only registered in the toolbar button instance to avoid the key press
50
+ // being double handled.
51
+ (0, react_2.useKeymap)('Mod-\\', handleShortcut);
52
+ return (react_1.default.createElement(react_1.default.Fragment, null,
53
+ react_1.default.createElement(react_components_1.VerticalDivider, null),
54
+ react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: false, isActive: false, icon: react_1.default.createElement(FormatClearRounded_1.default, null), label: `${contentSelected ? 'Clear formatting from selection' : 'Clear all formatting'} (cmd+\\)` })));
55
+ };
56
+ exports.default = ClearFormattingButton;
@@ -11,11 +11,11 @@ const JustifyAlignButton_1 = __importDefault(require("./JustifyAlign/JustifyAlig
11
11
  const react_components_1 = require("@remirror/react-components");
12
12
  const TextAlignButtons = () => {
13
13
  return (react_1.default.createElement(react_1.default.Fragment, null,
14
- react_1.default.createElement(react_components_1.VerticalDivider, { className: "editor-divider" }),
14
+ react_1.default.createElement(react_components_1.VerticalDivider, null),
15
15
  react_1.default.createElement(LeftAlignButton_1.default, null),
16
16
  react_1.default.createElement(CenterAlignButton_1.default, null),
17
17
  react_1.default.createElement(RightAlignButton_1.default, null),
18
18
  react_1.default.createElement(JustifyAlignButton_1.default, null),
19
- react_1.default.createElement(react_components_1.VerticalDivider, { className: "editor-divider" })));
19
+ react_1.default.createElement(react_components_1.VerticalDivider, null)));
20
20
  };
21
21
  exports.default = TextAlignButtons;
@@ -35,6 +35,6 @@ const TextTypeDropdown = () => {
35
35
  react_1.default.createElement(HeadingButton_1.default, { level: 5 }),
36
36
  react_1.default.createElement(HeadingButton_1.default, { level: 6 }),
37
37
  react_1.default.createElement(PreformattedButton_1.default, null)),
38
- react_1.default.createElement(react_2.VerticalDivider, { className: "editor-divider" })));
38
+ react_1.default.createElement(react_2.VerticalDivider, null)));
39
39
  };
40
40
  exports.default = TextTypeDropdown;
@@ -0,0 +1,5 @@
1
+ import { PlainExtension, CommandFunction } from '@remirror/core';
2
+ export declare class ClearFormattingExtension extends PlainExtension {
3
+ get name(): "clearFormatting";
4
+ clearFormatting(): CommandFunction;
5
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ClearFormattingExtension = void 0;
10
+ const core_1 = require("@remirror/core");
11
+ const getNodeNamesByGroup_1 = require("../../utils/getNodeNamesByGroup");
12
+ const getMarkNamesByGroup_1 = require("../../utils/getMarkNamesByGroup");
13
+ let ClearFormattingExtension = class ClearFormattingExtension extends core_1.PlainExtension {
14
+ get name() {
15
+ return 'clearFormatting';
16
+ }
17
+ clearFormatting() {
18
+ return ({ dispatch, tr, state }) => {
19
+ const { empty, ranges } = state.selection;
20
+ const schema = state.schema;
21
+ const formattingNodes = (0, getNodeNamesByGroup_1.getNodeNamesByGroup)(schema, core_1.ExtensionTag.FormattingNode);
22
+ const formattingMarks = (0, getMarkNamesByGroup_1.getMarkNamesByGroup)(schema, core_1.ExtensionTag.FormattingMark);
23
+ let isChanged = false;
24
+ ranges.forEach(({ $from, $to }) => {
25
+ // Check if there is a selection or not, if no selection use the doc content size as the range
26
+ state.doc.nodesBetween(empty ? 0 : $from.pos, empty ? state.doc.content.size : $to.pos, (node, pos) => {
27
+ // Clear marks (bold, italic, etc)
28
+ node.marks.forEach((mark) => {
29
+ if (formattingMarks.includes(mark.type.name)) {
30
+ tr.removeMark(pos, pos + node.nodeSize, mark);
31
+ isChanged = true;
32
+ }
33
+ });
34
+ // Leave non-foramtting nodes as-is
35
+ if (!formattingNodes.includes(node.type.name)) {
36
+ return;
37
+ }
38
+ // Clear node attributes & set to paragraph by default
39
+ if (node.type.name === schema.nodes.paragraph.name) {
40
+ const { nodeTextAlignment } = node.attrs;
41
+ if (nodeTextAlignment && nodeTextAlignment !== 'left') {
42
+ tr.setNodeAttribute(pos, 'nodeTextAlignment', null);
43
+ isChanged = true;
44
+ }
45
+ }
46
+ else {
47
+ tr.setNodeMarkup(pos, schema.nodes.paragraph, null, node.marks);
48
+ isChanged = true;
49
+ }
50
+ });
51
+ });
52
+ dispatch?.(tr);
53
+ return isChanged;
54
+ };
55
+ }
56
+ };
57
+ __decorate([
58
+ (0, core_1.command)()
59
+ ], ClearFormattingExtension.prototype, "clearFormatting", null);
60
+ ClearFormattingExtension = __decorate([
61
+ (0, core_1.extension)({})
62
+ ], ClearFormattingExtension);
63
+ exports.ClearFormattingExtension = ClearFormattingExtension;
@@ -8,6 +8,7 @@ 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 ClearFormattingExtension_1 = require("./ClearFormattingExtension/ClearFormattingExtension");
11
12
  var NodeName;
12
13
  (function (NodeName) {
13
14
  NodeName["Image"] = "image";
@@ -40,6 +41,7 @@ const createExtensions = (context) => {
40
41
  new AssetLinkExtension_1.AssetLinkExtension({
41
42
  matrixDomain: context.matrix.matrixDomain,
42
43
  }),
44
+ new ClearFormattingExtension_1.ClearFormattingExtension(),
43
45
  ];
44
46
  };
45
47
  };
package/lib/index.css CHANGED
@@ -812,12 +812,12 @@
812
812
  display: flex;
813
813
  justify-items: center;
814
814
  }
815
- .squiz-fte-scope .editor-toolbar > *:not(:first-child, .editor-divider),
816
- .squiz-fte-scope .squiz-fte-scope__floating-popover > *:not(:first-child, .editor-divider) {
815
+ .squiz-fte-scope .editor-toolbar > *:not(:first-child, .MuiDivider-root),
816
+ .squiz-fte-scope__floating-popover > *:not(:first-child, .MuiDivider-root) {
817
817
  margin: 0 0 0 2px;
818
818
  }
819
- .squiz-fte-scope .editor-toolbar .editor-divider,
820
- .squiz-fte-scope .squiz-fte-scope__floating-popover .editor-divider {
819
+ .squiz-fte-scope .editor-toolbar .MuiDivider-root,
820
+ .squiz-fte-scope__floating-popover .MuiDivider-root {
821
821
  margin-top: -0.25rem;
822
822
  margin-bottom: -0.25rem;
823
823
  margin-left: 0.25rem;
@@ -827,12 +827,12 @@
827
827
  height: auto;
828
828
  }
829
829
  .squiz-fte-scope .editor-toolbar .squiz-fte-btn,
830
- .squiz-fte-scope .squiz-fte-scope__floating-popover .squiz-fte-btn {
830
+ .squiz-fte-scope__floating-popover .squiz-fte-btn {
831
831
  padding: 0.25rem;
832
832
  font-weight: 700;
833
833
  }
834
834
  .squiz-fte-scope .editor-toolbar .squiz-fte-btn ~ .squiz-fte-btn,
835
- .squiz-fte-scope .squiz-fte-scope__floating-popover .squiz-fte-btn ~ .squiz-fte-btn {
835
+ .squiz-fte-scope__floating-popover .squiz-fte-btn ~ .squiz-fte-btn {
836
836
  margin-left: 2px;
837
837
  }
838
838
  .squiz-fte-scope__floating-popover {
@@ -851,7 +851,7 @@
851
851
  var(--tw-ring-shadow, 0 0 #0000),
852
852
  var(--tw-shadow);
853
853
  }
854
- .squiz-fte-scope .squiz-fte-scope__floating-popover .editor-divider {
854
+ .squiz-fte-scope__floating-popover .MuiDivider-root {
855
855
  margin-top: 0px;
856
856
  margin-bottom: 0px;
857
857
  }
@@ -1042,12 +1042,12 @@
1042
1042
  font-weight: 700;
1043
1043
  }
1044
1044
  .squiz-fte-scope .editor-toolbar .squiz-fte-modal-footer__button,
1045
- .squiz-fte-scope .squiz-fte-scope__floating-popover .squiz-fte-modal-footer__button {
1045
+ .squiz-fte-scope__floating-popover .squiz-fte-modal-footer__button {
1046
1046
  padding: 0.25rem;
1047
1047
  font-weight: 700;
1048
1048
  }
1049
1049
  .squiz-fte-scope .editor-toolbar .squiz-fte-modal-footer__button ~ .squiz-fte-btn,
1050
- .squiz-fte-scope .squiz-fte-scope__floating-popover .squiz-fte-modal-footer__button ~ .squiz-fte-btn {
1050
+ .squiz-fte-scope__floating-popover .squiz-fte-modal-footer__button ~ .squiz-fte-btn {
1051
1051
  margin-left: 2px;
1052
1052
  }
1053
1053
  .squiz-fte-scope .squiz-fte-modal-footer__button {
@@ -0,0 +1,2 @@
1
+ import { EditorSchema, ExtensionTagType } from '@remirror/core';
2
+ export declare const getMarkNamesByGroup: (schema: EditorSchema, group: ExtensionTagType) => string[];
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMarkNamesByGroup = void 0;
4
+ const getMarkNamesByGroup = (schema, group) => {
5
+ return Object.values(schema.marks)
6
+ .filter((mark) => mark.spec.group?.includes(group))
7
+ .map((mark) => mark.name);
8
+ };
9
+ exports.getMarkNamesByGroup = getMarkNamesByGroup;
@@ -0,0 +1,2 @@
1
+ import { EditorSchema, ExtensionTagType } from '@remirror/core';
2
+ export declare const getNodeNamesByGroup: (schema: EditorSchema, group: ExtensionTagType) => string[];
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNodeNamesByGroup = void 0;
4
+ const getNodeNamesByGroup = (schema, group) => {
5
+ return Object.values(schema.nodes)
6
+ .filter((node) => node.spec.group?.includes(group))
7
+ .map((node) => node.name);
8
+ };
9
+ exports.getNodeNamesByGroup = getNodeNamesByGroup;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "1.34.1-alpha.4",
3
+ "version": "1.34.1-alpha.6",
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.4",
24
- "@squiz/resource-browser": "1.34.1-alpha.4",
23
+ "@squiz/dx-json-schema-lib": "1.34.1-alpha.6",
24
+ "@squiz/resource-browser": "1.34.1-alpha.6",
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": "e36e955c6a306cbefb716f76e6cb986643c44c29"
78
+ "gitHead": "82f279a64c198e72d7ccfb6296624052bd4ce510"
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;
@@ -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
  };
@@ -36,7 +36,7 @@ const TextTypeDropdown = () => {
36
36
  <HeadingButton level={6} />
37
37
  <PreformattedButton />
38
38
  </ToolbarDropdown>
39
- <VerticalDivider className="editor-divider" />
39
+ <VerticalDivider />
40
40
  </>
41
41
  );
42
42
  };
@@ -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
+ }
@@ -15,6 +15,7 @@ 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 { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
18
19
 
19
20
  export enum NodeName {
20
21
  Image = 'image',
@@ -48,6 +49,7 @@ export const createExtensions = (context: EditorContextOptions) => {
48
49
  new AssetLinkExtension({
49
50
  matrixDomain: context.matrix.matrixDomain,
50
51
  }),
52
+ new ClearFormattingExtension(),
51
53
  ];
52
54
  };
53
55
  };
@@ -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>`,
@@ -0,0 +1,20 @@
1
+ import { EditorSchema, ExtensionTag } from '@remirror/core';
2
+ import { renderWithEditor } from '../../tests';
3
+ import { getMarkNamesByGroup } from './getMarkNamesByGroup';
4
+
5
+ describe('getMarkNamesGroup', () => {
6
+ it('Should return expected extensions with "FormattingMark" tag', async () => {
7
+ const { editor } = await renderWithEditor(null);
8
+ const schema: EditorSchema = editor.schema;
9
+ const formattingMarkNames = getMarkNamesByGroup(schema, ExtensionTag.FormattingMark);
10
+ const otherMarkNames = Object.values(schema.marks)
11
+ .map((mark) => mark.name)
12
+ .filter((mark) => !formattingMarkNames.includes(mark));
13
+
14
+ // This tag is used by the clear formatting extension.
15
+ // Marks in the first array will be transformed to a paragraph when formatting is cleared.
16
+ // Mark in the second array will be left as-is.
17
+ expect(formattingMarkNames).toEqual(['bold', 'italic', 'underline']);
18
+ expect(otherMarkNames).toEqual(['assetLink', 'link']);
19
+ });
20
+ });
@@ -0,0 +1,7 @@
1
+ import { EditorSchema, ExtensionTagType } from '@remirror/core';
2
+
3
+ export const getMarkNamesByGroup = (schema: EditorSchema, group: ExtensionTagType): string[] => {
4
+ return Object.values(schema.marks)
5
+ .filter((mark) => mark.spec.group?.includes(group))
6
+ .map((mark) => mark.name);
7
+ };
@@ -0,0 +1,20 @@
1
+ import { EditorSchema, ExtensionTag } from '@remirror/core';
2
+ import { renderWithEditor } from '../../tests';
3
+ import { getNodeNamesByGroup } from './getNodeNamesByGroup';
4
+
5
+ describe('getNodeNamesByGroup', () => {
6
+ it('Should return expected extensions with "FormattingNode" tag', async () => {
7
+ const { editor } = await renderWithEditor(null);
8
+ const schema: EditorSchema = editor.schema;
9
+ const formattingNodeNames = getNodeNamesByGroup(schema, ExtensionTag.FormattingNode);
10
+ const otherNodeNames = Object.values(schema.nodes)
11
+ .map((node) => node.name)
12
+ .filter((node) => !formattingNodeNames.includes(node));
13
+
14
+ // This tag is used by the clear formatting extension.
15
+ // Nodes in the first array will be transformed to a paragraph when formatting is cleared.
16
+ // Nodes in the second array will be left as-is.
17
+ expect(formattingNodeNames).toEqual(['paragraph', 'heading', 'preformatted']);
18
+ expect(otherNodeNames).toEqual(['assetImage', 'doc', 'text', 'image']);
19
+ });
20
+ });
@@ -0,0 +1,7 @@
1
+ import { EditorSchema, ExtensionTagType } from '@remirror/core';
2
+
3
+ export const getNodeNamesByGroup = (schema: EditorSchema, group: ExtensionTagType): string[] => {
4
+ return Object.values(schema.nodes)
5
+ .filter((node) => node.spec.group?.includes(group))
6
+ .map((node) => node.name);
7
+ };