@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.
- package/lib/Editor/Editor.js +5 -1
- package/lib/EditorToolbar/EditorToolbar.js +2 -0
- package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.d.ts +5 -0
- package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.js +32 -0
- package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.d.ts +2 -0
- package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.js +17 -0
- package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.d.ts +2 -0
- package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +16 -0
- package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.d.ts +2 -0
- package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +35 -0
- package/lib/extensions/PreformattedExtension/PreformattedExtension.d.ts +10 -0
- package/lib/extensions/PreformattedExtension/PreformattedExtension.js +46 -0
- package/lib/index.css +27 -0
- package/lib/ui/DropdownButton/DropdownButton.d.ts +9 -0
- package/lib/ui/DropdownButton/DropdownButton.js +8 -0
- package/lib/ui/ToolbarDropdown/ToolbarDropdown.d.ts +6 -0
- package/lib/ui/ToolbarDropdown/ToolbarDropdown.js +13 -0
- package/package.json +3 -2
- package/src/Editor/Editor.spec.tsx +140 -0
- package/src/Editor/Editor.tsx +6 -0
- package/src/EditorToolbar/EditorToolbar.tsx +2 -0
- package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.tsx +52 -0
- package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.tsx +26 -0
- package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.tsx +30 -0
- package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +44 -0
- package/src/extensions/PreformattedExtension/PreformattedExtension.tsx +50 -0
- package/src/index.scss +2 -0
- package/src/ui/DropdownButton/DropdownButton.tsx +28 -0
- package/src/ui/DropdownButton/_dropdown-button.scss +7 -0
- package/src/ui/ToolbarButton/ToolbarButton.tsx +1 -0
- package/src/ui/ToolbarDropdown/ToolbarDropdown.tsx +27 -0
- package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +25 -0
- package/tailwind.config.cjs +9 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +9 -1
package/lib/Editor/Editor.js
CHANGED
@@ -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,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,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,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,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,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.
|
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": "
|
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
|
});
|
package/src/Editor/Editor.tsx
CHANGED
@@ -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
@@ -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,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
|
+
}
|
package/tailwind.config.cjs
CHANGED
@@ -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