@squiz/formatted-text-editor 1.12.0-alpha.27 → 1.12.0-alpha.32

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 (35) hide show
  1. package/lib/Editor/Editor.js +5 -1
  2. package/lib/EditorToolbar/EditorToolbar.js +2 -0
  3. package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.d.ts +5 -0
  4. package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.js +32 -0
  5. package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.d.ts +2 -0
  6. package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.js +17 -0
  7. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.d.ts +2 -0
  8. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +16 -0
  9. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.d.ts +2 -0
  10. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +35 -0
  11. package/lib/extensions/PreformattedExtension/PreformattedExtension.d.ts +10 -0
  12. package/lib/extensions/PreformattedExtension/PreformattedExtension.js +46 -0
  13. package/lib/index.css +27 -0
  14. package/lib/ui/DropdownButton/DropdownButton.d.ts +9 -0
  15. package/lib/ui/DropdownButton/DropdownButton.js +8 -0
  16. package/lib/ui/ToolbarDropdown/ToolbarDropdown.d.ts +6 -0
  17. package/lib/ui/ToolbarDropdown/ToolbarDropdown.js +13 -0
  18. package/package.json +3 -2
  19. package/src/Editor/Editor.spec.tsx +140 -0
  20. package/src/Editor/Editor.tsx +6 -0
  21. package/src/EditorToolbar/EditorToolbar.tsx +2 -0
  22. package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.tsx +52 -0
  23. package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.tsx +26 -0
  24. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.tsx +30 -0
  25. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +44 -0
  26. package/src/extensions/PreformattedExtension/PreformattedExtension.tsx +50 -0
  27. package/src/index.scss +2 -0
  28. package/src/ui/DropdownButton/DropdownButton.tsx +28 -0
  29. package/src/ui/DropdownButton/_dropdown-button.scss +7 -0
  30. package/src/ui/ToolbarButton/ToolbarButton.tsx +1 -0
  31. package/src/ui/ToolbarDropdown/ToolbarDropdown.tsx +27 -0
  32. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +25 -0
  33. package/tailwind.config.cjs +9 -0
  34. package/tsconfig.json +1 -0
  35. package/vite.config.ts +9 -1
@@ -1,14 +1,18 @@
1
1
  import React from 'react';
2
- import { BoldExtension, ItalicExtension, NodeFormattingExtension, UnderlineExtension, HistoryExtension, wysiwygPreset, } from 'remirror/extensions';
2
+ import { BoldExtension, HeadingExtension, ItalicExtension, NodeFormattingExtension, ParagraphExtension, UnderlineExtension, HistoryExtension, wysiwygPreset, } from 'remirror/extensions';
3
3
  import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
4
4
  import { EditorToolbar } from '../EditorToolbar/EditorToolbar';
5
+ import { PreformattedExtension } from '../extensions/PreformattedExtension/PreformattedExtension';
5
6
  const Editor = ({ content }) => {
6
7
  const { manager, state, setState } = useRemirror({
7
8
  extensions: () => [
8
9
  ...wysiwygPreset(),
9
10
  new BoldExtension(),
11
+ new HeadingExtension(),
10
12
  new ItalicExtension(),
11
13
  new NodeFormattingExtension(),
14
+ new ParagraphExtension(),
15
+ new PreformattedExtension(),
12
16
  new UnderlineExtension(),
13
17
  new HistoryExtension(),
14
18
  ],
@@ -6,6 +6,7 @@ import BoldButton from './Tools/Bold/BoldButton';
6
6
  import TextAlignButtons from './Tools/TextAlign/TextAlignButtons';
7
7
  import UndoButton from './Tools/Undo/UndoButton';
8
8
  import RedoButton from './Tools/Redo/RedoButton';
9
+ import TextTypeDropdown from './Tools/TextType/TextTypeDropdown';
9
10
  // The editor main toolbar
10
11
  export const EditorToolbar = ({ manager, isPopover }) => {
11
12
  const extensionNames = {};
@@ -17,6 +18,7 @@ export const EditorToolbar = ({ manager, isPopover }) => {
17
18
  React.createElement(UndoButton, null),
18
19
  React.createElement(RedoButton, null),
19
20
  React.createElement(VerticalDivider, { className: "editor-divider" }))),
21
+ extensionNames.heading && extensionNames.paragraph && extensionNames.preformatted && React.createElement(TextTypeDropdown, null),
20
22
  extensionNames.bold && React.createElement(BoldButton, null),
21
23
  extensionNames.italic && React.createElement(ItalicButton, null),
22
24
  extensionNames.underline && React.createElement(UnderlineButton, null),
@@ -0,0 +1,5 @@
1
+ type HeadingButtonProps = {
2
+ level: number;
3
+ };
4
+ declare const HeadingButton: ({ level }: HeadingButtonProps) => JSX.Element;
5
+ export default HeadingButton;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands, useActive } from '@remirror/react';
3
+ import DropdownButton from '../../../../ui/DropdownButton/DropdownButton';
4
+ const HeadingButton = ({ level }) => {
5
+ const { toggleHeading } = useCommands();
6
+ const chain = useChainedCommands();
7
+ const active = useActive();
8
+ const enabled = toggleHeading.enabled({ level });
9
+ const handleSelect = () => {
10
+ if (toggleHeading.enabled({ level })) {
11
+ chain.toggleHeading({ level }).focus().run();
12
+ }
13
+ };
14
+ const headingContent = () => {
15
+ switch (level) {
16
+ case 1:
17
+ return React.createElement("h1", null, "Heading 1");
18
+ case 2:
19
+ return React.createElement("h2", null, "Heading 2");
20
+ case 3:
21
+ return React.createElement("h3", null, "Heading 3");
22
+ case 4:
23
+ return React.createElement("h4", null, "Heading 4");
24
+ case 5:
25
+ return React.createElement("h5", null, "Heading 5");
26
+ case 6:
27
+ return React.createElement("h6", null, "Heading 6");
28
+ }
29
+ };
30
+ return (React.createElement(DropdownButton, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.heading({ level }), label: `Heading ${level}` }, headingContent()));
31
+ };
32
+ export default HeadingButton;
@@ -0,0 +1,2 @@
1
+ declare const ParagraphButton: () => JSX.Element;
2
+ export default ParagraphButton;
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands, useActive } from '@remirror/react';
3
+ import DropdownButton from '../../../../ui/DropdownButton/DropdownButton';
4
+ const ParagraphButton = () => {
5
+ const { convertParagraph } = useCommands();
6
+ const chain = useChainedCommands();
7
+ const active = useActive();
8
+ const enabled = convertParagraph.enabled();
9
+ const handleSelect = () => {
10
+ if (convertParagraph.enabled()) {
11
+ chain.convertParagraph().focus().run();
12
+ }
13
+ };
14
+ return (React.createElement(DropdownButton, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.paragraph(), label: "Paragraph" },
15
+ React.createElement("p", null, "Paragraph")));
16
+ };
17
+ export default ParagraphButton;
@@ -0,0 +1,2 @@
1
+ declare const PreformattedButton: () => JSX.Element;
2
+ export default PreformattedButton;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive } from '@remirror/react';
3
+ import DropdownButton from '../../../../ui/DropdownButton/DropdownButton';
4
+ const PreformattedButton = () => {
5
+ const { togglePreformatted } = useCommands();
6
+ const active = useActive();
7
+ const enabled = togglePreformatted.enabled();
8
+ const handleSelect = () => {
9
+ if (togglePreformatted.enabled()) {
10
+ togglePreformatted();
11
+ }
12
+ };
13
+ return (React.createElement(DropdownButton, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.preformatted(), label: "Preformatted" },
14
+ React.createElement("pre", null, "Preformatted")));
15
+ };
16
+ export default PreformattedButton;
@@ -0,0 +1,2 @@
1
+ declare const TextTypeDropdown: () => JSX.Element;
2
+ export default TextTypeDropdown;
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import HeadingButton from './Heading/HeadingButton';
3
+ import ParagraphButton from './Paragraph/ParagraphButton';
4
+ import PreformattedButton from './Preformatted/PreformattedButton';
5
+ import ToolbarDropdown from '../../../ui/ToolbarDropdown/ToolbarDropdown';
6
+ import { useActive, VerticalDivider } from '@remirror/react';
7
+ const TextTypeDropdown = () => {
8
+ const active = useActive();
9
+ const activeLabel = () => {
10
+ // Determine if preformatted is active
11
+ if (active.preformatted()) {
12
+ return 'Preformatted';
13
+ }
14
+ // Determine if a heading is active
15
+ for (let i = 1; i <= 6; i++) {
16
+ if (active.heading({ level: i })) {
17
+ return `Heading ${i}`;
18
+ }
19
+ }
20
+ // Default to paragraph
21
+ return 'Paragraph';
22
+ };
23
+ return (React.createElement(React.Fragment, null,
24
+ React.createElement(ToolbarDropdown, { label: activeLabel() },
25
+ React.createElement(ParagraphButton, null),
26
+ React.createElement(HeadingButton, { level: 1 }),
27
+ React.createElement(HeadingButton, { level: 2 }),
28
+ React.createElement(HeadingButton, { level: 3 }),
29
+ React.createElement(HeadingButton, { level: 4 }),
30
+ React.createElement(HeadingButton, { level: 5 }),
31
+ React.createElement(HeadingButton, { level: 6 }),
32
+ React.createElement(PreformattedButton, null)),
33
+ React.createElement(VerticalDivider, { className: "editor-divider" })));
34
+ };
35
+ export default TextTypeDropdown;
@@ -0,0 +1,10 @@
1
+ import { ApplySchemaAttributes, CommandFunction, NodeExtension, NodeExtensionSpec, NodeSpecOverride } from '@remirror/core';
2
+ export declare class PreformattedExtension extends NodeExtension {
3
+ get name(): "preformatted";
4
+ createTags(): ("formattingNode" | "block" | "textBlock")[];
5
+ createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec;
6
+ /**
7
+ * Toggle the <pre> for the current block.
8
+ */
9
+ togglePreformatted(): CommandFunction;
10
+ }
@@ -0,0 +1,46 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { command, extension, ExtensionTag, NodeExtension, toggleBlockItem, } from '@remirror/core';
8
+ let PreformattedExtension = class PreformattedExtension extends NodeExtension {
9
+ get name() {
10
+ return 'preformatted';
11
+ }
12
+ createTags() {
13
+ return [ExtensionTag.Block, ExtensionTag.TextBlock, ExtensionTag.FormattingNode];
14
+ }
15
+ createNodeSpec(extra, override) {
16
+ return {
17
+ content: 'inline*',
18
+ defining: true,
19
+ draggable: false,
20
+ ...override,
21
+ attrs: {
22
+ ...extra.defaults(),
23
+ },
24
+ parseDOM: [...(override.parseDOM ?? [])],
25
+ toDOM: (node) => {
26
+ return [`pre`, extra.dom(node), 0];
27
+ },
28
+ };
29
+ }
30
+ /**
31
+ * Toggle the <pre> for the current block.
32
+ */
33
+ togglePreformatted() {
34
+ return toggleBlockItem({
35
+ type: this.type,
36
+ toggleType: 'paragraph',
37
+ });
38
+ }
39
+ };
40
+ __decorate([
41
+ command()
42
+ ], PreformattedExtension.prototype, "togglePreformatted", null);
43
+ PreformattedExtension = __decorate([
44
+ extension({})
45
+ ], PreformattedExtension);
46
+ export { PreformattedExtension };
package/lib/index.css CHANGED
@@ -3835,3 +3835,30 @@ button:active .remirror-menu-pane-shortcut,
3835
3835
  .toolbar-button:active {
3836
3836
  @apply text-blue-300 bg-blue-100;
3837
3837
  }
3838
+ .toolbar-dropdown {
3839
+ align-self: center;
3840
+ }
3841
+ .toolbar-dropdown .toolbar-dropdown__button {
3842
+ @apply font-base text-md font-semibold text-gray-600;
3843
+ height: 2rem;
3844
+ padding-left: 0.5rem;
3845
+ }
3846
+ .toolbar-dropdown .toolbar-dropdown__button:active,
3847
+ .toolbar-dropdown .toolbar-dropdown__button:hover,
3848
+ .toolbar-dropdown .toolbar-dropdown__button:focus {
3849
+ background-color: rgba(0, 0, 0, 0.04);
3850
+ }
3851
+ .toolbar-dropdown .toolbar-dropdown__button .toolbar-dropdown__label {
3852
+ padding-right: 0.25rem;
3853
+ }
3854
+ .toolbar-dropdown .toolbar-dropdown__button .toolbar-dropdown__icon {
3855
+ width: 1rem;
3856
+ height: 1.5rem;
3857
+ }
3858
+ .dropdown-button {
3859
+ height: 2.5rem;
3860
+ padding: 0.25rem 0.5rem;
3861
+ justify-content: space-between;
3862
+ border-radius: 0;
3863
+ width: 100%;
3864
+ }
@@ -0,0 +1,9 @@
1
+ type DropdownButtonProps = {
2
+ children?: JSX.Element;
3
+ handleOnClick: () => void;
4
+ isDisabled: boolean;
5
+ isActive: boolean;
6
+ label: string;
7
+ };
8
+ declare const DropdownButton: ({ children, handleOnClick, isDisabled, isActive, label }: DropdownButtonProps) => JSX.Element;
9
+ export default DropdownButton;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import CheckIcon from '@mui/icons-material/Check';
3
+ const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }) => {
4
+ return (React.createElement("button", { "aria-label": label, title: label, type: "button", onClick: handleOnClick, disabled: isDisabled, className: `btn dropdown-button ${isActive ? 'is-active' : ''}` },
5
+ children || label,
6
+ isActive && React.createElement(CheckIcon, { className: "dropdown-button-icon" })));
7
+ };
8
+ export default DropdownButton;
@@ -0,0 +1,6 @@
1
+ type ToolbarDropdownProps = {
2
+ children: JSX.Element | JSX.Element[];
3
+ label: string;
4
+ };
5
+ declare const ToolbarDropdown: ({ children, label }: ToolbarDropdownProps) => JSX.Element;
6
+ export default ToolbarDropdown;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Menu } from '@headlessui/react';
3
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
4
+ const ToolbarDropdown = ({ children, label }) => {
5
+ return (React.createElement(Menu, { as: "div", className: "toolbar-dropdown" },
6
+ React.createElement("div", null,
7
+ React.createElement(Menu.Button, { className: "toolbar-dropdown__button" },
8
+ React.createElement("span", { className: "toolbar-dropdown__label" }, label),
9
+ React.createElement(ExpandMoreIcon, { className: "toolbar-dropdown__icon", "aria-hidden": "true" }))),
10
+ React.createElement(Menu.Items, { className: "fixed left-20 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-md " },
11
+ React.createElement("div", { className: "py-1" }, children))));
12
+ };
13
+ export default ToolbarDropdown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "1.12.0-alpha.27",
3
+ "version": "1.12.0-alpha.32",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -16,6 +16,7 @@
16
16
  "test:e2e": "vite build && vite preview --port 8080 & cypress open"
17
17
  },
18
18
  "dependencies": {
19
+ "@headlessui/react": "^1.7.10",
19
20
  "@mui/icons-material": "5.11.0",
20
21
  "@remirror/react": "2.0.25"
21
22
  },
@@ -60,5 +61,5 @@
60
61
  "volta": {
61
62
  "node": "16.19.0"
62
63
  },
63
- "gitHead": "ff692c1b6cea88887d9f3bb67c54658408e03037"
64
+ "gitHead": "527320f3d27039e18f96135807a9b09e3a19e305"
64
65
  }
@@ -85,4 +85,144 @@ describe('Formatted text editor', () => {
85
85
  fireEvent.click(justifyAlignButton);
86
86
  expect(baseElement.querySelector('p[data-node-text-align="justify"]')).toBeTruthy();
87
87
  });
88
+
89
+ it('Applies Heading 1 styling to text when clicked', () => {
90
+ const { baseElement } = render(<Editor />);
91
+ expect(baseElement).toBeTruthy();
92
+
93
+ expect(baseElement.querySelector('div.remirror-editor h1')).toBeFalsy();
94
+
95
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
96
+ expect(headingDropdown).toBeTruthy();
97
+ fireEvent.click(headingDropdown);
98
+
99
+ const h1Button = baseElement.querySelector('button[title="Heading 1"]') as HTMLButtonElement;
100
+ expect(h1Button).toBeTruthy();
101
+ fireEvent.click(h1Button);
102
+
103
+ expect(baseElement.querySelector('div.remirror-editor h1')).toBeTruthy();
104
+ });
105
+
106
+ it('Applies Heading 2 styling to text when clicked', () => {
107
+ const { baseElement } = render(<Editor />);
108
+ expect(baseElement).toBeTruthy();
109
+
110
+ expect(baseElement.querySelector('div.remirror-editor h2')).toBeFalsy();
111
+
112
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
113
+ expect(headingDropdown).toBeTruthy();
114
+ fireEvent.click(headingDropdown);
115
+
116
+ const h2Button = baseElement.querySelector('button[title="Heading 2"]') as HTMLButtonElement;
117
+ expect(h2Button).toBeTruthy();
118
+ fireEvent.click(h2Button);
119
+
120
+ expect(baseElement.querySelector('div.remirror-editor h2')).toBeTruthy();
121
+ });
122
+
123
+ it('Applies Heading 3 styling to text when clicked', () => {
124
+ const { baseElement } = render(<Editor />);
125
+ expect(baseElement).toBeTruthy();
126
+
127
+ expect(baseElement.querySelector('div.remirror-editor h3')).toBeFalsy();
128
+
129
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
130
+ expect(headingDropdown).toBeTruthy();
131
+ fireEvent.click(headingDropdown);
132
+
133
+ const h3Button = baseElement.querySelector('button[title="Heading 3"]') as HTMLButtonElement;
134
+ expect(h3Button).toBeTruthy();
135
+ fireEvent.click(h3Button);
136
+
137
+ expect(baseElement.querySelector('div.remirror-editor h3')).toBeTruthy();
138
+ });
139
+
140
+ it('Applies Heading 4 styling to text when clicked', () => {
141
+ const { baseElement } = render(<Editor />);
142
+ expect(baseElement).toBeTruthy();
143
+
144
+ expect(baseElement.querySelector('div.remirror-editor h4')).toBeFalsy();
145
+
146
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
147
+ expect(headingDropdown).toBeTruthy();
148
+ fireEvent.click(headingDropdown);
149
+
150
+ const h4Button = baseElement.querySelector('button[title="Heading 4"]') as HTMLButtonElement;
151
+ expect(h4Button).toBeTruthy();
152
+ fireEvent.click(h4Button);
153
+
154
+ expect(baseElement.querySelector('div.remirror-editor h4')).toBeTruthy();
155
+ });
156
+
157
+ it('Applies Heading 5 styling to text when clicked', () => {
158
+ const { baseElement } = render(<Editor />);
159
+ expect(baseElement).toBeTruthy();
160
+
161
+ expect(baseElement.querySelector('div.remirror-editor h5')).toBeFalsy();
162
+
163
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
164
+ expect(headingDropdown).toBeTruthy();
165
+ fireEvent.click(headingDropdown);
166
+
167
+ const h5Button = baseElement.querySelector('button[title="Heading 5"]') as HTMLButtonElement;
168
+ expect(h5Button).toBeTruthy();
169
+ fireEvent.click(h5Button);
170
+
171
+ expect(baseElement.querySelector('div.remirror-editor h5')).toBeTruthy();
172
+ });
173
+
174
+ it('Applies Heading 6 styling to text when clicked', () => {
175
+ const { baseElement } = render(<Editor />);
176
+ expect(baseElement).toBeTruthy();
177
+
178
+ expect(baseElement.querySelector('div.remirror-editor h6')).toBeFalsy();
179
+
180
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
181
+ expect(headingDropdown).toBeTruthy();
182
+ fireEvent.click(headingDropdown);
183
+
184
+ const h6Button = baseElement.querySelector('button[title="Heading 6"]') as HTMLButtonElement;
185
+ expect(h6Button).toBeTruthy();
186
+ fireEvent.click(h6Button);
187
+
188
+ expect(baseElement.querySelector('div.remirror-editor h6')).toBeTruthy();
189
+ });
190
+
191
+ it('Applies Preformatted styling to text when clicked', () => {
192
+ const { baseElement } = render(<Editor />);
193
+ expect(baseElement).toBeTruthy();
194
+
195
+ expect(baseElement.querySelector('div.remirror-editor pre')).toBeFalsy();
196
+
197
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
198
+ expect(headingDropdown).toBeTruthy();
199
+ fireEvent.click(headingDropdown);
200
+
201
+ const preButton = baseElement.querySelector('button[title="Preformatted"]') as HTMLButtonElement;
202
+ expect(preButton).toBeTruthy();
203
+ fireEvent.click(preButton);
204
+
205
+ expect(baseElement.querySelector('div.remirror-editor pre')).toBeTruthy();
206
+ });
207
+
208
+ it('Applies Paragraph styling to text when clicked', () => {
209
+ const { baseElement } = render(<Editor />);
210
+ expect(baseElement).toBeTruthy();
211
+
212
+ expect(baseElement.querySelectorAll('div.remirror-editor p')).toHaveLength(1);
213
+
214
+ const headingDropdown = baseElement.querySelector('.toolbar-dropdown button') as HTMLButtonElement;
215
+ expect(headingDropdown).toBeTruthy();
216
+ fireEvent.click(headingDropdown);
217
+
218
+ const preButton = baseElement.querySelector('button[title="Preformatted"]') as HTMLButtonElement;
219
+ expect(preButton).toBeTruthy();
220
+ fireEvent.click(preButton);
221
+
222
+ const paragraphButton = baseElement.querySelector('button[title="Paragraph"]') as HTMLButtonElement;
223
+ expect(paragraphButton).toBeTruthy();
224
+ fireEvent.click(paragraphButton);
225
+
226
+ expect(baseElement.querySelectorAll('div.remirror-editor p')).toHaveLength(2);
227
+ });
88
228
  });
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
2
  import {
3
3
  BoldExtension,
4
+ HeadingExtension,
4
5
  ItalicExtension,
5
6
  NodeFormattingExtension,
7
+ ParagraphExtension,
6
8
  UnderlineExtension,
7
9
  HistoryExtension,
8
10
  wysiwygPreset,
@@ -10,6 +12,7 @@ import {
10
12
  import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
11
13
  import { RemirrorContentType, Extension } from '@remirror/core';
12
14
  import { EditorToolbar } from '../EditorToolbar/EditorToolbar';
15
+ import { PreformattedExtension } from '../extensions/PreformattedExtension/PreformattedExtension';
13
16
 
14
17
  type EditorProps = {
15
18
  content?: RemirrorContentType;
@@ -20,8 +23,11 @@ const Editor = ({ content }: EditorProps) => {
20
23
  extensions: () => [
21
24
  ...(wysiwygPreset() as Extension[]),
22
25
  new BoldExtension(),
26
+ new HeadingExtension(),
23
27
  new ItalicExtension(),
24
28
  new NodeFormattingExtension(),
29
+ new ParagraphExtension(),
30
+ new PreformattedExtension(),
25
31
  new UnderlineExtension(),
26
32
  new HistoryExtension(),
27
33
  ],
@@ -7,6 +7,7 @@ import BoldButton from './Tools/Bold/BoldButton';
7
7
  import TextAlignButtons from './Tools/TextAlign/TextAlignButtons';
8
8
  import UndoButton from './Tools/Undo/UndoButton';
9
9
  import RedoButton from './Tools/Redo/RedoButton';
10
+ import TextTypeDropdown from './Tools/TextType/TextTypeDropdown';
10
11
 
11
12
  type EditorToolbarProps = {
12
13
  manager: RemirrorManager<any>;
@@ -32,6 +33,7 @@ export const EditorToolbar = ({ manager, isPopover }: EditorToolbarProps) => {
32
33
  <VerticalDivider className="editor-divider" />
33
34
  </>
34
35
  )}
36
+ {extensionNames.heading && extensionNames.paragraph && extensionNames.preformatted && <TextTypeDropdown />}
35
37
  {extensionNames.bold && <BoldButton />}
36
38
  {extensionNames.italic && <ItalicButton />}
37
39
  {extensionNames.underline && <UnderlineButton />}
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands, useActive } from '@remirror/react';
3
+ import { HeadingExtension } from '@remirror/extension-heading';
4
+ import DropdownButton from '../../../../ui/DropdownButton/DropdownButton';
5
+
6
+ type HeadingButtonProps = {
7
+ level: number;
8
+ };
9
+
10
+ const HeadingButton = ({ level }: HeadingButtonProps) => {
11
+ const { toggleHeading } = useCommands<HeadingExtension>();
12
+ const chain = useChainedCommands();
13
+
14
+ const active = useActive<HeadingExtension>();
15
+ const enabled = toggleHeading.enabled({ level });
16
+
17
+ const handleSelect = () => {
18
+ if (toggleHeading.enabled({ level })) {
19
+ chain.toggleHeading({ level }).focus().run();
20
+ }
21
+ };
22
+
23
+ const headingContent = () => {
24
+ switch (level) {
25
+ case 1:
26
+ return <h1>Heading 1</h1>;
27
+ case 2:
28
+ return <h2>Heading 2</h2>;
29
+ case 3:
30
+ return <h3>Heading 3</h3>;
31
+ case 4:
32
+ return <h4>Heading 4</h4>;
33
+ case 5:
34
+ return <h5>Heading 5</h5>;
35
+ case 6:
36
+ return <h6>Heading 6</h6>;
37
+ }
38
+ };
39
+
40
+ return (
41
+ <DropdownButton
42
+ handleOnClick={handleSelect}
43
+ isDisabled={!enabled}
44
+ isActive={active.heading({ level })}
45
+ label={`Heading ${level}`}
46
+ >
47
+ {headingContent()}
48
+ </DropdownButton>
49
+ );
50
+ };
51
+
52
+ export default HeadingButton;
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands, useActive } from '@remirror/react';
3
+ import { ParagraphExtension } from '@remirror/extension-paragraph';
4
+ import DropdownButton from '../../../../ui/DropdownButton/DropdownButton';
5
+
6
+ const ParagraphButton = () => {
7
+ const { convertParagraph } = useCommands<ParagraphExtension>();
8
+ const chain = useChainedCommands();
9
+
10
+ const active = useActive<ParagraphExtension>();
11
+ const enabled = convertParagraph.enabled();
12
+
13
+ const handleSelect = () => {
14
+ if (convertParagraph.enabled()) {
15
+ chain.convertParagraph().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <DropdownButton handleOnClick={handleSelect} isDisabled={!enabled} isActive={active.paragraph()} label="Paragraph">
21
+ <p>Paragraph</p>
22
+ </DropdownButton>
23
+ );
24
+ };
25
+
26
+ export default ParagraphButton;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive } from '@remirror/react';
3
+ import { PreformattedExtension } from '../../../../extensions/PreformattedExtension/PreformattedExtension';
4
+ import DropdownButton from '../../../../ui/DropdownButton/DropdownButton';
5
+
6
+ const PreformattedButton = () => {
7
+ const { togglePreformatted } = useCommands<PreformattedExtension>();
8
+
9
+ const active = useActive<PreformattedExtension>();
10
+ const enabled = togglePreformatted.enabled();
11
+
12
+ const handleSelect = () => {
13
+ if (togglePreformatted.enabled()) {
14
+ togglePreformatted();
15
+ }
16
+ };
17
+
18
+ return (
19
+ <DropdownButton
20
+ handleOnClick={handleSelect}
21
+ isDisabled={!enabled}
22
+ isActive={active.preformatted()}
23
+ label="Preformatted"
24
+ >
25
+ <pre>Preformatted</pre>
26
+ </DropdownButton>
27
+ );
28
+ };
29
+
30
+ export default PreformattedButton;
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import HeadingButton from './Heading/HeadingButton';
3
+ import ParagraphButton from './Paragraph/ParagraphButton';
4
+ import PreformattedButton from './Preformatted/PreformattedButton';
5
+ import ToolbarDropdown from '../../../ui/ToolbarDropdown/ToolbarDropdown';
6
+ import { useActive, VerticalDivider } from '@remirror/react';
7
+ import { PreformattedExtension } from '../../../extensions/PreformattedExtension/PreformattedExtension';
8
+
9
+ const TextTypeDropdown = () => {
10
+ const active = useActive<PreformattedExtension>();
11
+
12
+ const activeLabel = () => {
13
+ // Determine if preformatted is active
14
+ if (active.preformatted()) {
15
+ return 'Preformatted';
16
+ }
17
+ // Determine if a heading is active
18
+ for (let i = 1; i <= 6; i++) {
19
+ if (active.heading({ level: i })) {
20
+ return `Heading ${i}`;
21
+ }
22
+ }
23
+ // Default to paragraph
24
+ return 'Paragraph';
25
+ };
26
+
27
+ return (
28
+ <>
29
+ <ToolbarDropdown label={activeLabel()}>
30
+ <ParagraphButton />
31
+ <HeadingButton level={1} />
32
+ <HeadingButton level={2} />
33
+ <HeadingButton level={3} />
34
+ <HeadingButton level={4} />
35
+ <HeadingButton level={5} />
36
+ <HeadingButton level={6} />
37
+ <PreformattedButton />
38
+ </ToolbarDropdown>
39
+ <VerticalDivider className="editor-divider" />
40
+ </>
41
+ );
42
+ };
43
+
44
+ export default TextTypeDropdown;
@@ -0,0 +1,50 @@
1
+ import {
2
+ ApplySchemaAttributes,
3
+ command,
4
+ CommandFunction,
5
+ extension,
6
+ ExtensionTag,
7
+ NodeExtension,
8
+ NodeExtensionSpec,
9
+ NodeSpecOverride,
10
+ ProsemirrorNode,
11
+ toggleBlockItem,
12
+ } from '@remirror/core';
13
+
14
+ @extension({})
15
+ export class PreformattedExtension extends NodeExtension {
16
+ get name() {
17
+ return 'preformatted' as const;
18
+ }
19
+
20
+ createTags() {
21
+ return [ExtensionTag.Block, ExtensionTag.TextBlock, ExtensionTag.FormattingNode];
22
+ }
23
+
24
+ createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
25
+ return {
26
+ content: 'inline*',
27
+ defining: true,
28
+ draggable: false,
29
+ ...override,
30
+ attrs: {
31
+ ...extra.defaults(),
32
+ },
33
+ parseDOM: [...(override.parseDOM ?? [])],
34
+ toDOM: (node: ProsemirrorNode) => {
35
+ return [`pre`, extra.dom(node), 0];
36
+ },
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Toggle the <pre> for the current block.
42
+ */
43
+ @command()
44
+ togglePreformatted(): CommandFunction {
45
+ return toggleBlockItem({
46
+ type: this.type,
47
+ toggleType: 'paragraph',
48
+ });
49
+ }
50
+ }
package/src/index.scss CHANGED
@@ -11,3 +11,5 @@
11
11
  @import './EditorToolbar/editor-toolbar';
12
12
 
13
13
  @import './ui/ToolbarButton/toolbar-button';
14
+ @import './ui/ToolbarDropdown/toolbar-dropdown';
15
+ @import './ui/DropdownButton/dropdown-button';
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import CheckIcon from '@mui/icons-material/Check';
3
+
4
+ type DropdownButtonProps = {
5
+ children?: JSX.Element;
6
+ handleOnClick: () => void;
7
+ isDisabled: boolean;
8
+ isActive: boolean;
9
+ label: string;
10
+ };
11
+
12
+ const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }: DropdownButtonProps) => {
13
+ return (
14
+ <button
15
+ aria-label={label}
16
+ title={label}
17
+ type="button"
18
+ onClick={handleOnClick}
19
+ disabled={isDisabled}
20
+ className={`btn dropdown-button ${isActive ? 'is-active' : ''}`}
21
+ >
22
+ {children || label}
23
+ {isActive && <CheckIcon className="dropdown-button-icon" />}
24
+ </button>
25
+ );
26
+ };
27
+
28
+ export default DropdownButton;
@@ -0,0 +1,7 @@
1
+ .dropdown-button {
2
+ height: 2.5rem;
3
+ padding: 0.25rem 0.5rem;
4
+ justify-content: space-between;
5
+ border-radius: 0;
6
+ width: 100%;
7
+ }
@@ -7,6 +7,7 @@ type ToolbarButtonProps = {
7
7
  icon: ReactElement;
8
8
  label: string;
9
9
  };
10
+
10
11
  const ToolbarButton = ({ handleOnClick, isDisabled, isActive, icon, label }: ToolbarButtonProps) => {
11
12
  return (
12
13
  <button
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { Menu } from '@headlessui/react';
3
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
4
+
5
+ type ToolbarDropdownProps = {
6
+ children: JSX.Element | JSX.Element[];
7
+ label: string;
8
+ };
9
+
10
+ const ToolbarDropdown = ({ children, label }: ToolbarDropdownProps) => {
11
+ return (
12
+ <Menu as="div" className="toolbar-dropdown">
13
+ <div>
14
+ <Menu.Button className="toolbar-dropdown__button">
15
+ <span className="toolbar-dropdown__label">{label}</span>
16
+ <ExpandMoreIcon className="toolbar-dropdown__icon" aria-hidden="true" />
17
+ </Menu.Button>
18
+ </div>
19
+
20
+ <Menu.Items className="fixed left-20 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-md ">
21
+ <div className="py-1">{children}</div>
22
+ </Menu.Items>
23
+ </Menu>
24
+ );
25
+ };
26
+
27
+ export default ToolbarDropdown;
@@ -0,0 +1,25 @@
1
+ .toolbar-dropdown {
2
+ align-self: center;
3
+
4
+ .toolbar-dropdown__button {
5
+ @apply font-base text-md font-semibold text-gray-600;
6
+
7
+ height: 2rem;
8
+ padding-left: 0.5rem;
9
+
10
+ &:active,
11
+ &:hover,
12
+ &:focus {
13
+ background-color: rgba(black, 0.04);
14
+ }
15
+
16
+ .toolbar-dropdown__label {
17
+ padding-right: 0.25rem;
18
+ }
19
+
20
+ .toolbar-dropdown__icon {
21
+ width: 1rem;
22
+ height: 1.5rem;
23
+ }
24
+ }
25
+ }
@@ -7,12 +7,21 @@ module.exports = {
7
7
  DEFAULT: '4px',
8
8
  md: '6px',
9
9
  },
10
+ fontFamily: {
11
+ base: 'Open Sans, Arial, sans-serif',
12
+ },
10
13
  fontWeight: {
11
14
  normal: '400',
12
15
  medium: '500',
13
16
  semibold: '600',
14
17
  bold: '700',
15
18
  },
19
+ fontSize: {
20
+ xlg: '1.125rem',
21
+ lg: '1rem',
22
+ md: '0.875rem',
23
+ sm: '0.8125rem',
24
+ },
16
25
  boxShadow: {
17
26
  outline: '0 0 0 1px rgba(0,0,0,0.10)',
18
27
  sm: '0 0 0 1px rgba(0,0,0,0.04), 0 1px 4px 2px rgba(0,0,0,0.08)',
package/tsconfig.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
4
  "useDefineForClassFields": true,
5
+ "experimentalDecorators": true,
5
6
  "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
7
  "allowJs": false,
7
8
  "skipLibCheck": true,
package/vite.config.ts CHANGED
@@ -7,5 +7,13 @@ export default defineConfig({
7
7
  build: {
8
8
  outDir: 'build/demo',
9
9
  },
10
- plugins: [react()],
10
+ plugins: [
11
+ react({
12
+ babel: {
13
+ parserOpts: {
14
+ plugins: ['decorators-legacy'],
15
+ },
16
+ },
17
+ }),
18
+ ],
11
19
  });