@squiz/formatted-text-editor 1.35.1-alpha.6 → 1.38.0-alpha.2
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/demo/App.tsx +15 -1
- package/lib/Editor/Editor.d.ts +4 -1
- package/lib/Editor/Editor.js +13 -4
- package/lib/EditorToolbar/FloatingToolbar.js +1 -1
- package/lib/EditorToolbar/Toolbar.d.ts +5 -1
- package/lib/EditorToolbar/Toolbar.js +4 -3
- package/lib/Extensions/UnsuportedExtension/UnsupportedNodeExtension.js +1 -1
- package/lib/hooks/useFocus.d.ts +8 -0
- package/lib/hooks/useFocus.js +18 -0
- package/lib/index.css +57 -35
- package/package.json +4 -4
- package/src/Editor/Editor.spec.tsx +43 -0
- package/src/Editor/Editor.tsx +24 -6
- package/src/Editor/_editor.scss +4 -0
- package/src/EditorToolbar/FloatingToolbar.tsx +6 -1
- package/src/EditorToolbar/Toolbar.tsx +11 -3
- package/src/EditorToolbar/_toolbar.scss +15 -2
- package/src/Extensions/UnsuportedExtension/UnsupportedNodeExtension.spec.ts +2 -2
- package/src/Extensions/UnsuportedExtension/UnsupportedNodeExtension.tsx +1 -1
- package/src/hooks/useFocus.ts +28 -0
- package/src/ui/Button/_button.scss +1 -1
- package/src/ui/CollapseBox/_collapseBox.scss +1 -1
- package/src/ui/_typography.scss +62 -60
package/demo/App.tsx
CHANGED
@@ -3,8 +3,20 @@ import { Editor, remirrorNodeToSquizNode, squizNodeToRemirrorNode } from '../src
|
|
3
3
|
import { RemirrorEventListener, Extension } from '@remirror/core';
|
4
4
|
import ReactDiffViewer from 'react-diff-viewer-continued';
|
5
5
|
import { AppContext } from './AppContext';
|
6
|
+
import Button from '../src/ui/Button/Button';
|
7
|
+
import TextFieldsOutlinedIcon from '@mui/icons-material/TextFieldsOutlined';
|
8
|
+
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
9
|
+
import { VerticalDivider } from '@remirror/react-components';
|
10
|
+
const ComponentHandlers = () => (
|
11
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '5px' }}>
|
12
|
+
<Button icon={<TextFieldsOutlinedIcon />} onClick={(x) => x} />
|
13
|
+
<VerticalDivider />
|
14
|
+
<Button icon={<DeleteOutlineOutlinedIcon />} onClick={(x) => x} />
|
15
|
+
</div>
|
16
|
+
);
|
6
17
|
|
7
18
|
function App() {
|
19
|
+
const [showChildren] = useState(false);
|
8
20
|
const [doc, setDoc] = useState('');
|
9
21
|
const [squizDoc, setSquizDoc] = useState('');
|
10
22
|
const [reconvertedDoc, setReconvertedDoc] = useState('');
|
@@ -41,7 +53,9 @@ function App() {
|
|
41
53
|
editable={editable}
|
42
54
|
content={`<p>Hello <a href="https://www.google.com"><strong>Mr Bean</strong></a>, nice to <a href="https://www.google.com">meet you</a>.<img src="https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif" height="150" width="200"/></p>`}
|
43
55
|
onChange={handleEditorChange}
|
44
|
-
|
56
|
+
>
|
57
|
+
{showChildren && <ComponentHandlers />}
|
58
|
+
</Editor>
|
45
59
|
</AppContext>
|
46
60
|
</div>
|
47
61
|
<h1>Document</h1>
|
package/lib/Editor/Editor.d.ts
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
import { ReactNode } from 'react';
|
1
2
|
import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
|
2
3
|
type EditorProps = {
|
3
4
|
content?: RemirrorContentType;
|
4
5
|
onChange?: RemirrorEventListener<Extension>;
|
5
6
|
editable?: boolean;
|
7
|
+
children?: ReactNode;
|
8
|
+
isFocused?: boolean;
|
6
9
|
};
|
7
|
-
declare const Editor: ({ content, editable, onChange }: EditorProps) => JSX.Element;
|
10
|
+
declare const Editor: ({ content, editable, onChange, children, isFocused }: EditorProps) => JSX.Element;
|
8
11
|
export default Editor;
|
package/lib/Editor/Editor.js
CHANGED
@@ -28,10 +28,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
29
29
|
const react_1 = __importStar(require("react"));
|
30
30
|
const react_2 = require("@remirror/react");
|
31
|
+
const clsx_1 = __importDefault(require("clsx"));
|
31
32
|
const EditorToolbar_1 = require("../EditorToolbar");
|
32
33
|
const EditorContext_1 = require("./EditorContext");
|
33
34
|
const Extensions_1 = require("../Extensions/Extensions");
|
34
|
-
const
|
35
|
+
const useFocus_1 = __importDefault(require("../hooks/useFocus"));
|
35
36
|
const WrappedEditor = () => {
|
36
37
|
const preventImagePaste = (0, react_1.useCallback)((event) => {
|
37
38
|
const { clipboardData } = event;
|
@@ -45,7 +46,7 @@ const WrappedEditor = () => {
|
|
45
46
|
(0, react_2.useEditorEvent)('paste', preventImagePaste);
|
46
47
|
return react_1.default.createElement(react_2.EditorComponent, null);
|
47
48
|
};
|
48
|
-
const Editor = ({ content, editable = true, onChange }) => {
|
49
|
+
const Editor = ({ content, editable = true, onChange, children, isFocused }) => {
|
49
50
|
const { manager, state, setState } = (0, react_2.useRemirror)({
|
50
51
|
extensions: (0, Extensions_1.createExtensions)((0, react_1.useContext)(EditorContext_1.EditorContext)),
|
51
52
|
content,
|
@@ -56,10 +57,18 @@ const Editor = ({ content, editable = true, onChange }) => {
|
|
56
57
|
setState(parameter.state);
|
57
58
|
onChange?.(parameter);
|
58
59
|
};
|
60
|
+
const { isVisible, handleFocus, handleBlur, wrapperRef } = (0, useFocus_1.default)(isFocused || false);
|
61
|
+
// On initial load, check if we need to focus the actual text content
|
62
|
+
(0, react_1.useEffect)(() => {
|
63
|
+
if (isFocused) {
|
64
|
+
manager.view.dom.focus();
|
65
|
+
}
|
66
|
+
}, []);
|
59
67
|
return (react_1.default.createElement("div", { className: "squiz-fte-scope" },
|
60
|
-
react_1.default.createElement("div", { className: (0, clsx_1.default)('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled') },
|
68
|
+
react_1.default.createElement("div", { ref: wrapperRef, onBlur: handleBlur, onFocusCapture: handleFocus, className: (0, clsx_1.default)('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled') },
|
61
69
|
react_1.default.createElement(react_2.Remirror, { manager: manager, state: state, editable: editable, onChange: handleChange, placeholder: "Write something", label: "Text editor" },
|
62
|
-
editable && react_1.default.createElement(EditorToolbar_1.Toolbar,
|
70
|
+
editable && react_1.default.createElement(EditorToolbar_1.Toolbar, { isVisible: isVisible }),
|
71
|
+
children,
|
63
72
|
react_1.default.createElement(WrappedEditor, null),
|
64
73
|
editable && react_1.default.createElement(EditorToolbar_1.FloatingToolbar, null)))));
|
65
74
|
};
|
@@ -71,6 +71,6 @@ const FloatingToolbar = () => {
|
|
71
71
|
if (extensionNames.clearFormatting && clearFormatting.enabled()) {
|
72
72
|
buttons.push(react_1.default.createElement(ClearFormattingButton_1.default, { key: "clearFormatting" }));
|
73
73
|
}
|
74
|
-
return (react_1.default.createElement(react_2.FloatingToolbar, { className: "squiz-fte-scope squiz-fte-scope__floating-popover", positioner: positioner }, buttons));
|
74
|
+
return (react_1.default.createElement(react_2.FloatingToolbar, { className: "squiz-fte-scope squiz-fte-scope__floating-popover", positioner: positioner, tabIndex: 0, role: 'toolbar' }, buttons));
|
75
75
|
};
|
76
76
|
exports.FloatingToolbar = FloatingToolbar;
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Toolbar = void 0;
|
7
7
|
const react_1 = __importDefault(require("react"));
|
8
8
|
const react_components_1 = require("@remirror/react-components");
|
9
|
+
const clsx_1 = __importDefault(require("clsx"));
|
9
10
|
const ItalicButton_1 = __importDefault(require("./Tools/Italic/ItalicButton"));
|
10
11
|
const UnderlineButton_1 = __importDefault(require("./Tools/Underline/UnderlineButton"));
|
11
12
|
const BoldButton_1 = __importDefault(require("./Tools/Bold/BoldButton"));
|
@@ -13,14 +14,14 @@ const TextAlignButtons_1 = __importDefault(require("./Tools/TextAlign/TextAlignB
|
|
13
14
|
const UndoButton_1 = __importDefault(require("./Tools/Undo/UndoButton"));
|
14
15
|
const RedoButton_1 = __importDefault(require("./Tools/Redo/RedoButton"));
|
15
16
|
const TextTypeDropdown_1 = __importDefault(require("./Tools/TextType/TextTypeDropdown"));
|
16
|
-
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
20
|
const ClearFormattingButton_1 = __importDefault(require("./Tools/ClearFormatting/ClearFormattingButton"));
|
21
|
-
const
|
21
|
+
const hooks_1 = require("../hooks");
|
22
|
+
const Toolbar = ({ isVisible }) => {
|
22
23
|
const extensionNames = (0, hooks_1.useExtensionNames)();
|
23
|
-
return (react_1.default.createElement(react_components_1.Toolbar, { className:
|
24
|
+
return (react_1.default.createElement(react_components_1.Toolbar, { className: (0, clsx_1.default)('remirror-toolbar editor-toolbar header-toolbar', isVisible && 'show-toolbar'), role: "toolbar", tabIndex: 0 },
|
24
25
|
extensionNames.history && (react_1.default.createElement(react_1.default.Fragment, null,
|
25
26
|
react_1.default.createElement(UndoButton_1.default, null),
|
26
27
|
react_1.default.createElement(RedoButton_1.default, null),
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { FocusEventHandler, RefObject } from 'react';
|
2
|
+
declare const useFocus: (initialState: boolean) => {
|
3
|
+
handleFocus: () => void;
|
4
|
+
handleBlur: FocusEventHandler<HTMLDivElement>;
|
5
|
+
isVisible: boolean;
|
6
|
+
wrapperRef: RefObject<HTMLDivElement>;
|
7
|
+
};
|
8
|
+
export default useFocus;
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const react_1 = require("react");
|
4
|
+
const useFocus = (initialState) => {
|
5
|
+
const wrapperRef = (0, react_1.createRef)();
|
6
|
+
const [isVisible, setIsVisible] = (0, react_1.useState)(initialState);
|
7
|
+
const handleFocus = (0, react_1.useCallback)(() => {
|
8
|
+
setIsVisible(true);
|
9
|
+
}, []);
|
10
|
+
const handleBlur = (event) => {
|
11
|
+
const isOutside = wrapperRef.current !== null && !wrapperRef.current.contains(event.relatedTarget);
|
12
|
+
if (isOutside) {
|
13
|
+
setIsVisible(false);
|
14
|
+
}
|
15
|
+
};
|
16
|
+
return { handleFocus, handleBlur, isVisible, wrapperRef };
|
17
|
+
};
|
18
|
+
exports.default = useFocus;
|
package/lib/index.css
CHANGED
@@ -638,71 +638,71 @@
|
|
638
638
|
.squiz-fte-scope .filter {
|
639
639
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
|
640
640
|
}
|
641
|
-
.squiz-fte-scope a {
|
641
|
+
.squiz-fte-scope .remirror-editor a {
|
642
642
|
--tw-text-opacity: 1;
|
643
643
|
color: rgb(7 116 210 / var(--tw-text-opacity));
|
644
644
|
text-decoration: underline;
|
645
645
|
}
|
646
|
-
.squiz-fte-scope h1 {
|
646
|
+
.squiz-fte-scope .remirror-editor h1 {
|
647
647
|
font-size: 1.625rem;
|
648
648
|
font-weight: 600;
|
649
649
|
letter-spacing: -0.2px;
|
650
650
|
line-height: 2rem;
|
651
651
|
}
|
652
|
-
.squiz-fte-scope h2 {
|
652
|
+
.squiz-fte-scope .remirror-editor h2 {
|
653
653
|
font-size: 1.25rem;
|
654
654
|
font-weight: 600;
|
655
655
|
letter-spacing: -0.5px;
|
656
656
|
line-height: 1.5rem;
|
657
657
|
}
|
658
|
-
.squiz-fte-scope h3 {
|
658
|
+
.squiz-fte-scope .remirror-editor h3 {
|
659
659
|
font-size: 1.125rem;
|
660
660
|
font-weight: 600;
|
661
661
|
letter-spacing: -0.2px;
|
662
662
|
line-height: 1.375rem;
|
663
663
|
}
|
664
|
-
.squiz-fte-scope h4 {
|
664
|
+
.squiz-fte-scope .remirror-editor h4 {
|
665
665
|
font-size: 1rem;
|
666
666
|
font-weight: 700;
|
667
667
|
letter-spacing: -0.2px;
|
668
668
|
line-height: 1.25rem;
|
669
669
|
}
|
670
|
-
.squiz-fte-scope h5 {
|
670
|
+
.squiz-fte-scope .remirror-editor h5 {
|
671
671
|
font-size: 1rem;
|
672
672
|
font-weight: 600;
|
673
673
|
letter-spacing: -0.2px;
|
674
674
|
line-height: 1.25rem;
|
675
675
|
}
|
676
|
-
.squiz-fte-scope h6 {
|
676
|
+
.squiz-fte-scope .remirror-editor h6 {
|
677
677
|
font-size: 0.875rem;
|
678
678
|
font-weight: 600;
|
679
679
|
letter-spacing: -0.2px;
|
680
680
|
line-height: 1.25rem;
|
681
681
|
}
|
682
|
-
.squiz-fte-scope .code-block,
|
683
|
-
.squiz-fte-scope .preformatted {
|
682
|
+
.squiz-fte-scope .remirror-editor .code-block,
|
683
|
+
.squiz-fte-scope .remirror-editor .preformatted {
|
684
684
|
display: flex;
|
685
685
|
}
|
686
|
-
.squiz-fte-scope .code-block .material-symbols-rounded,
|
687
|
-
.squiz-fte-scope .preformatted .material-symbols-rounded {
|
686
|
+
.squiz-fte-scope .remirror-editor .code-block .material-symbols-rounded,
|
687
|
+
.squiz-fte-scope .remirror-editor .preformatted .material-symbols-rounded {
|
688
688
|
font-size: 1.125rem;
|
689
689
|
--tw-text-opacity: 1;
|
690
690
|
color: rgb(79 79 79 / var(--tw-text-opacity));
|
691
691
|
pointer-events: none;
|
692
692
|
margin-right: 0.375rem;
|
693
693
|
}
|
694
|
-
.squiz-fte-scope .code-block .block-divider,
|
695
|
-
.squiz-fte-scope .preformatted .block-divider {
|
694
|
+
.squiz-fte-scope .remirror-editor .code-block .block-divider,
|
695
|
+
.squiz-fte-scope .remirror-editor .preformatted .block-divider {
|
696
696
|
--tw-bg-opacity: 1;
|
697
697
|
background-color: rgb(237 237 237 / var(--tw-bg-opacity));
|
698
698
|
width: 4px;
|
699
699
|
border-radius: 0.75rem;
|
700
700
|
margin-right: 0.625rem;
|
701
701
|
}
|
702
|
-
.squiz-fte-scope .code-block code,
|
703
|
-
.squiz-fte-scope .code-block pre,
|
704
|
-
.squiz-fte-scope .preformatted code,
|
705
|
-
.squiz-fte-scope .preformatted pre {
|
702
|
+
.squiz-fte-scope .remirror-editor .code-block code,
|
703
|
+
.squiz-fte-scope .remirror-editor .code-block pre,
|
704
|
+
.squiz-fte-scope .remirror-editor .preformatted code,
|
705
|
+
.squiz-fte-scope .remirror-editor .preformatted pre {
|
706
706
|
--tw-text-opacity: 1;
|
707
707
|
color: rgb(79 79 79 / var(--tw-text-opacity));
|
708
708
|
font-size: 0.75rem;
|
@@ -835,8 +835,19 @@
|
|
835
835
|
.squiz-fte-scope .formatted-text-editor img {
|
836
836
|
display: inline;
|
837
837
|
}
|
838
|
+
.squiz-fte-scope .formatted-text-editor:has(.show-toolbar) {
|
839
|
+
border-width: 0px;
|
840
|
+
--tw-shadow: 0 0 0 1px rgba(0,0,0,0.04), 0 1px 12px 4px rgba(0,0,0,0.12);
|
841
|
+
--tw-shadow-colored: 0 0 0 1px var(--tw-shadow-color), 0 1px 12px 4px var(--tw-shadow-color);
|
842
|
+
box-shadow:
|
843
|
+
var(--tw-ring-offset-shadow, 0 0 #0000),
|
844
|
+
var(--tw-ring-shadow, 0 0 #0000),
|
845
|
+
var(--tw-shadow);
|
846
|
+
}
|
838
847
|
.squiz-fte-scope .editor-toolbar,
|
839
848
|
.squiz-fte-scope__floating-popover {
|
849
|
+
border-top-left-radius: 4px;
|
850
|
+
border-top-right-radius: 4px;
|
840
851
|
border-bottom-width: 2px;
|
841
852
|
border-style: solid;
|
842
853
|
--tw-border-opacity: 1;
|
@@ -846,6 +857,7 @@
|
|
846
857
|
padding: 0.25rem;
|
847
858
|
display: flex;
|
848
859
|
justify-items: center;
|
860
|
+
flex-wrap: wrap;
|
849
861
|
}
|
850
862
|
.squiz-fte-scope .editor-toolbar > *:not(:first-child, .MuiDivider-root),
|
851
863
|
.squiz-fte-scope__floating-popover > *:not(:first-child, .MuiDivider-root) {
|
@@ -853,8 +865,8 @@
|
|
853
865
|
}
|
854
866
|
.squiz-fte-scope .editor-toolbar .MuiDivider-root,
|
855
867
|
.squiz-fte-scope__floating-popover .MuiDivider-root {
|
856
|
-
margin-top:
|
857
|
-
margin-bottom:
|
868
|
+
margin-top: 0px;
|
869
|
+
margin-bottom: 0px;
|
858
870
|
margin-left: 0.25rem;
|
859
871
|
margin-right: 0.25rem;
|
860
872
|
border-width: 1px;
|
@@ -870,6 +882,15 @@
|
|
870
882
|
.squiz-fte-scope__floating-popover .squiz-fte-btn ~ .squiz-fte-btn {
|
871
883
|
margin-left: 2px;
|
872
884
|
}
|
885
|
+
.squiz-fte-scope .header-toolbar {
|
886
|
+
transition: opacity 0.2s linear, max-height 0.2s linear;
|
887
|
+
opacity: 0;
|
888
|
+
max-height: 0;
|
889
|
+
}
|
890
|
+
.squiz-fte-scope .header-toolbar.show-toolbar {
|
891
|
+
opacity: 1;
|
892
|
+
max-height: 15vh;
|
893
|
+
}
|
873
894
|
.squiz-fte-scope__floating-popover {
|
874
895
|
display: flex;
|
875
896
|
border-radius: 6px;
|
@@ -890,7 +911,7 @@
|
|
890
911
|
margin-top: 0px;
|
891
912
|
margin-bottom: 0px;
|
892
913
|
}
|
893
|
-
.squiz-fte-scope .squiz-fte-btn {
|
914
|
+
.squiz-fte-scope button.squiz-fte-btn {
|
894
915
|
border-radius: 4px;
|
895
916
|
--tw-bg-opacity: 1;
|
896
917
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
@@ -912,23 +933,23 @@
|
|
912
933
|
border: 1px solid transparent;
|
913
934
|
padding: 6px 12px;
|
914
935
|
}
|
915
|
-
.squiz-fte-scope .squiz-fte-btn--is-icon {
|
936
|
+
.squiz-fte-scope button.squiz-fte-btn--is-icon {
|
916
937
|
padding: 6px;
|
917
938
|
}
|
918
|
-
.squiz-fte-scope .squiz-fte-btn ~ .squiz-fte-btn {
|
939
|
+
.squiz-fte-scope button.squiz-fte-btn ~ .squiz-fte-btn {
|
919
940
|
margin-left: 2px;
|
920
941
|
}
|
921
|
-
.squiz-fte-scope .squiz-fte-btn.disabled,
|
922
|
-
.squiz-fte-scope .squiz-fte-btn[disabled] {
|
942
|
+
.squiz-fte-scope button.squiz-fte-btn.disabled,
|
943
|
+
.squiz-fte-scope button.squiz-fte-btn[disabled] {
|
923
944
|
cursor: not-allowed;
|
924
945
|
opacity: 0.5;
|
925
946
|
}
|
926
|
-
.squiz-fte-scope .squiz-fte-btn:hover,
|
927
|
-
.squiz-fte-scope .squiz-fte-btn:focus {
|
947
|
+
.squiz-fte-scope button.squiz-fte-btn:hover,
|
948
|
+
.squiz-fte-scope button.squiz-fte-btn:focus {
|
928
949
|
background-color: rgba(0, 0, 0, 0.04);
|
929
950
|
}
|
930
|
-
.squiz-fte-scope .squiz-fte-btn--is-active,
|
931
|
-
.squiz-fte-scope .squiz-fte-btn:active {
|
951
|
+
.squiz-fte-scope button.squiz-fte-btn--is-active,
|
952
|
+
.squiz-fte-scope button.squiz-fte-btn:active {
|
932
953
|
--tw-bg-opacity: 1;
|
933
954
|
background-color: rgb(230 241 250 / var(--tw-bg-opacity));
|
934
955
|
--tw-text-opacity: 1;
|
@@ -1041,6 +1062,7 @@
|
|
1041
1062
|
.squiz-fte-scope .collapse-box__label {
|
1042
1063
|
margin-left: 0.5rem;
|
1043
1064
|
margin-right: auto;
|
1065
|
+
text-align: left;
|
1044
1066
|
}
|
1045
1067
|
.squiz-fte-scope .collapse-box__icon {
|
1046
1068
|
--tw-text-opacity: 1;
|
@@ -1126,7 +1148,7 @@
|
|
1126
1148
|
.squiz-fte-scope__floating-popover .squiz-fte-modal-footer__button ~ .squiz-fte-btn {
|
1127
1149
|
margin-left: 2px;
|
1128
1150
|
}
|
1129
|
-
.squiz-fte-scope .squiz-fte-modal-footer__button {
|
1151
|
+
.squiz-fte-scope button.squiz-fte-modal-footer__button {
|
1130
1152
|
border-radius: 4px;
|
1131
1153
|
--tw-bg-opacity: 1;
|
1132
1154
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
@@ -1148,19 +1170,19 @@
|
|
1148
1170
|
border: 1px solid transparent;
|
1149
1171
|
padding: 6px 12px;
|
1150
1172
|
}
|
1151
|
-
.squiz-fte-scope .squiz-fte-modal-footer__button ~ .squiz-fte-btn {
|
1173
|
+
.squiz-fte-scope button.squiz-fte-modal-footer__button ~ .squiz-fte-btn {
|
1152
1174
|
margin-left: 2px;
|
1153
1175
|
}
|
1154
|
-
.squiz-fte-scope .squiz-fte-modal-footer__button.disabled,
|
1155
|
-
.squiz-fte-scope .squiz-fte-modal-footer__button[disabled] {
|
1176
|
+
.squiz-fte-scope button.squiz-fte-modal-footer__button.disabled,
|
1177
|
+
.squiz-fte-scope button.squiz-fte-modal-footer__button[disabled] {
|
1156
1178
|
cursor: not-allowed;
|
1157
1179
|
opacity: 0.5;
|
1158
1180
|
}
|
1159
|
-
.squiz-fte-scope .squiz-fte-modal-footer__button:hover,
|
1160
|
-
.squiz-fte-scope .squiz-fte-modal-footer__button:focus {
|
1181
|
+
.squiz-fte-scope button.squiz-fte-modal-footer__button:hover,
|
1182
|
+
.squiz-fte-scope button.squiz-fte-modal-footer__button:focus {
|
1161
1183
|
background-color: rgba(0, 0, 0, 0.04);
|
1162
1184
|
}
|
1163
|
-
.squiz-fte-scope .squiz-fte-modal-footer__button:active {
|
1185
|
+
.squiz-fte-scope button.squiz-fte-modal-footer__button:active {
|
1164
1186
|
--tw-bg-opacity: 1;
|
1165
1187
|
background-color: rgb(230 241 250 / var(--tw-bg-opacity));
|
1166
1188
|
--tw-text-opacity: 1;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/formatted-text-editor",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.38.0-alpha.2",
|
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.
|
24
|
-
"@squiz/resource-browser": "1.
|
23
|
+
"@squiz/dx-json-schema-lib": "1.38.0-alpha.2",
|
24
|
+
"@squiz/resource-browser": "1.38.0-alpha.2",
|
25
25
|
"clsx": "1.2.1",
|
26
26
|
"react-hook-form": "7.43.2",
|
27
27
|
"react-image-size": "2.0.0",
|
@@ -75,5 +75,5 @@
|
|
75
75
|
"volta": {
|
76
76
|
"node": "18.15.0"
|
77
77
|
},
|
78
|
-
"gitHead": "
|
78
|
+
"gitHead": "9c2f775a7d6607a73e5b25577d82be939d74ffec"
|
79
79
|
}
|
@@ -5,7 +5,23 @@ import '@testing-library/jest-dom';
|
|
5
5
|
import { renderWithEditor } from '../../tests';
|
6
6
|
import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
|
7
7
|
|
8
|
+
const isVisible = jest.fn().mockReturnValue(false);
|
9
|
+
const handleFocusMock = jest.fn();
|
10
|
+
const handleBlurMock = jest.fn();
|
11
|
+
jest.mock('../hooks/useFocus', () => ({
|
12
|
+
__esModule: true,
|
13
|
+
default: () => ({
|
14
|
+
isVisible: isVisible(),
|
15
|
+
handleFocus: handleFocusMock,
|
16
|
+
handleBlur: handleBlurMock,
|
17
|
+
}),
|
18
|
+
}));
|
19
|
+
|
8
20
|
describe('Formatted text editor', () => {
|
21
|
+
afterEach(() => {
|
22
|
+
jest.clearAllMocks();
|
23
|
+
});
|
24
|
+
|
9
25
|
it('Renders the text editor', () => {
|
10
26
|
render(<Editor />);
|
11
27
|
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
@@ -311,4 +327,31 @@ describe('Formatted text editor', () => {
|
|
311
327
|
|
312
328
|
expect(buttonLabels).toEqual([]);
|
313
329
|
});
|
330
|
+
|
331
|
+
it('triggers handleFocus when editor is focused', async () => {
|
332
|
+
const { getByLabelText } = render(<Editor />);
|
333
|
+
const editorInput = getByLabelText('Text editor');
|
334
|
+
|
335
|
+
fireEvent.focus(editorInput);
|
336
|
+
|
337
|
+
expect(handleBlurMock).toHaveBeenCalledTimes(0);
|
338
|
+
expect(handleFocusMock).toHaveBeenCalledTimes(1);
|
339
|
+
});
|
340
|
+
|
341
|
+
it('triggers handleBlur when editor is blurred', () => {
|
342
|
+
const { getByLabelText } = render(<Editor />);
|
343
|
+
const editorInput = getByLabelText('Text editor');
|
344
|
+
|
345
|
+
fireEvent.blur(editorInput);
|
346
|
+
|
347
|
+
expect(handleBlurMock).toHaveBeenCalledTimes(1);
|
348
|
+
expect(handleFocusMock).toHaveBeenCalledTimes(0);
|
349
|
+
});
|
350
|
+
|
351
|
+
it('should apply hide class when focus hook returns false', () => {
|
352
|
+
isVisible.mockReturnValue(true);
|
353
|
+
const { container } = render(<Editor />);
|
354
|
+
|
355
|
+
expect(container.querySelector('.show-toolbar')).toBeInTheDocument();
|
356
|
+
});
|
314
357
|
});
|
package/src/Editor/Editor.tsx
CHANGED
@@ -1,16 +1,19 @@
|
|
1
|
-
import React, { useContext, useCallback } from 'react';
|
1
|
+
import React, { useContext, useCallback, ReactNode, useEffect } from 'react';
|
2
2
|
import { EditorComponent, Remirror, useRemirror, useEditorEvent } from '@remirror/react';
|
3
3
|
import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
|
4
|
+
import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
|
5
|
+
import clsx from 'clsx';
|
4
6
|
import { Toolbar, FloatingToolbar } from '../EditorToolbar';
|
5
7
|
import { EditorContext } from './EditorContext';
|
6
8
|
import { createExtensions } from '../Extensions/Extensions';
|
7
|
-
import
|
8
|
-
import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
|
9
|
+
import useFocus from '../hooks/useFocus';
|
9
10
|
|
10
11
|
type EditorProps = {
|
11
12
|
content?: RemirrorContentType;
|
12
13
|
onChange?: RemirrorEventListener<Extension>;
|
13
14
|
editable?: boolean;
|
15
|
+
children?: ReactNode;
|
16
|
+
isFocused?: boolean;
|
14
17
|
};
|
15
18
|
|
16
19
|
const WrappedEditor = () => {
|
@@ -29,7 +32,7 @@ const WrappedEditor = () => {
|
|
29
32
|
return <EditorComponent />;
|
30
33
|
};
|
31
34
|
|
32
|
-
const Editor = ({ content, editable = true, onChange }: EditorProps) => {
|
35
|
+
const Editor = ({ content, editable = true, onChange, children, isFocused }: EditorProps) => {
|
33
36
|
const { manager, state, setState } = useRemirror({
|
34
37
|
extensions: createExtensions(useContext(EditorContext)),
|
35
38
|
content,
|
@@ -42,9 +45,23 @@ const Editor = ({ content, editable = true, onChange }: EditorProps) => {
|
|
42
45
|
onChange?.(parameter);
|
43
46
|
};
|
44
47
|
|
48
|
+
const { isVisible, handleFocus, handleBlur, wrapperRef } = useFocus(isFocused || false);
|
49
|
+
|
50
|
+
// On initial load, check if we need to focus the actual text content
|
51
|
+
useEffect(() => {
|
52
|
+
if (isFocused) {
|
53
|
+
manager.view.dom.focus();
|
54
|
+
}
|
55
|
+
}, []);
|
56
|
+
|
45
57
|
return (
|
46
58
|
<div className="squiz-fte-scope">
|
47
|
-
<div
|
59
|
+
<div
|
60
|
+
ref={wrapperRef}
|
61
|
+
onBlur={handleBlur}
|
62
|
+
onFocusCapture={handleFocus}
|
63
|
+
className={clsx('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled')}
|
64
|
+
>
|
48
65
|
<Remirror
|
49
66
|
manager={manager}
|
50
67
|
state={state}
|
@@ -53,7 +70,8 @@ const Editor = ({ content, editable = true, onChange }: EditorProps) => {
|
|
53
70
|
placeholder="Write something"
|
54
71
|
label="Text editor"
|
55
72
|
>
|
56
|
-
{editable && <Toolbar />}
|
73
|
+
{editable && <Toolbar isVisible={isVisible} />}
|
74
|
+
{children}
|
57
75
|
<WrappedEditor />
|
58
76
|
{editable && <FloatingToolbar />}
|
59
77
|
</Remirror>
|
package/src/Editor/_editor.scss
CHANGED
@@ -50,7 +50,12 @@ export const FloatingToolbar = () => {
|
|
50
50
|
}
|
51
51
|
|
52
52
|
return (
|
53
|
-
<RemirrorFloatingToolbar
|
53
|
+
<RemirrorFloatingToolbar
|
54
|
+
className="squiz-fte-scope squiz-fte-scope__floating-popover"
|
55
|
+
positioner={positioner}
|
56
|
+
tabIndex={0}
|
57
|
+
role={'toolbar'}
|
58
|
+
>
|
54
59
|
{buttons}
|
55
60
|
</RemirrorFloatingToolbar>
|
56
61
|
);
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { Toolbar as RemirrorToolbar, VerticalDivider } from '@remirror/react-components';
|
3
|
+
import clsx from 'clsx';
|
3
4
|
import ItalicButton from './Tools/Italic/ItalicButton';
|
4
5
|
import UnderlineButton from './Tools/Underline/UnderlineButton';
|
5
6
|
import BoldButton from './Tools/Bold/BoldButton';
|
@@ -7,17 +8,24 @@ import TextAlignButtons from './Tools/TextAlign/TextAlignButtons';
|
|
7
8
|
import UndoButton from './Tools/Undo/UndoButton';
|
8
9
|
import RedoButton from './Tools/Redo/RedoButton';
|
9
10
|
import TextTypeDropdown from './Tools/TextType/TextTypeDropdown';
|
10
|
-
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
14
|
import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton';
|
15
|
+
import { useExtensionNames } from '../hooks';
|
15
16
|
|
16
|
-
|
17
|
+
type ToolbarProps = {
|
18
|
+
isVisible: boolean;
|
19
|
+
};
|
20
|
+
export const Toolbar = ({ isVisible }: ToolbarProps) => {
|
17
21
|
const extensionNames = useExtensionNames();
|
18
22
|
|
19
23
|
return (
|
20
|
-
<RemirrorToolbar
|
24
|
+
<RemirrorToolbar
|
25
|
+
className={clsx('remirror-toolbar editor-toolbar header-toolbar', isVisible && 'show-toolbar')}
|
26
|
+
role="toolbar"
|
27
|
+
tabIndex={0}
|
28
|
+
>
|
21
29
|
{extensionNames.history && (
|
22
30
|
<>
|
23
31
|
<UndoButton />
|
@@ -1,18 +1,20 @@
|
|
1
1
|
.editor-toolbar {
|
2
|
-
@apply bg-white border-gray-200 border-b-2 border-solid p-1;
|
2
|
+
@apply bg-white border-gray-200 border-b-2 border-solid p-1 rounded-t;
|
3
3
|
|
4
4
|
display: flex;
|
5
5
|
justify-items: center;
|
6
|
+
flex-wrap: wrap;
|
6
7
|
|
7
8
|
> *:not(:first-child, .MuiDivider-root) {
|
8
9
|
margin: 0 0 0 2px;
|
9
10
|
}
|
10
11
|
|
11
12
|
.MuiDivider-root {
|
12
|
-
@apply
|
13
|
+
@apply my-0 mx-1 border;
|
13
14
|
margin-right: 2px;
|
14
15
|
height: auto;
|
15
16
|
}
|
17
|
+
|
16
18
|
.squiz-fte-btn {
|
17
19
|
@apply p-1 font-bold;
|
18
20
|
~ .squiz-fte-btn {
|
@@ -20,3 +22,14 @@
|
|
20
22
|
}
|
21
23
|
}
|
22
24
|
}
|
25
|
+
|
26
|
+
.header-toolbar {
|
27
|
+
transition: opacity 0.2s linear, max-height 0.2s linear;
|
28
|
+
opacity: 0;
|
29
|
+
max-height: 0;
|
30
|
+
|
31
|
+
&.show-toolbar {
|
32
|
+
opacity: 1;
|
33
|
+
max-height: 15vh;
|
34
|
+
}
|
35
|
+
}
|
@@ -35,7 +35,7 @@ describe('UnsupportedNodeExtension', () => {
|
|
35
35
|
const nodeSpec = extension.createNodeSpec(extra, override);
|
36
36
|
|
37
37
|
expect(nodeSpec.selectable).toBe(true);
|
38
|
-
expect(nodeSpec.draggable).toBe(
|
38
|
+
expect(nodeSpec.draggable).toBe(false);
|
39
39
|
expect(nodeSpec.atom).toBe(true);
|
40
40
|
expect(nodeSpec.inline).toBe(true);
|
41
41
|
expect(nodeSpec.attrs).toEqual({ originalNode: {}, errorMessage: {} });
|
@@ -131,7 +131,7 @@ describe('UnsupportedNodeExtension', () => {
|
|
131
131
|
});
|
132
132
|
|
133
133
|
expect(getHtmlContent()).toBe(
|
134
|
-
'<span class="unsupported-node-node-view-wrapper" originalnode="null" errormessage="null" data-unsupported-node="{"originalNode":null,"errorMessage":null}"
|
134
|
+
'<span class="unsupported-node-node-view-wrapper" originalnode="null" errormessage="null" data-unsupported-node="{"originalNode":null,"errorMessage":null}"><div class="collapse-box" contenteditable="false"><button class="collapse-box__header" type="button"><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium collapse-box__icon--warning css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="MotionPhotosOffOutlinedIcon"><path d="M2.81 2.81 1.39 4.22l2.27 2.27C2.61 8.07 2 9.96 2 12c0 5.52 4.48 10 10 10 2.04 0 3.93-.61 5.51-1.66l2.27 2.27 1.41-1.42L2.81 2.81zM12 20c-4.41 0-8-3.59-8-8 0-1.48.41-2.86 1.12-4.06l10.93 10.94C14.86 19.59 13.48 20 12 20zm0-16c4.41 0 8 3.59 8 8 0 1.48-.41 2.86-1.12 4.05l1.45 1.45C21.39 15.93 22 14.04 22 12c0-5.52-4.48-10-10-10-2.04 0-3.93.61-5.51 1.66l1.45 1.45C9.14 4.41 10.52 4 12 4z"></path></svg><div class="collapse-box__label">This section cannot be displayed here due to unsupported HTML elements. The front-end view of your page won’t be affected.</div><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="UnfoldLessOutlinedIcon"><path d="M7.41 18.59 8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z"></path></svg></button><div class="collapse-box__content" hidden="" data-testid="content"><br>null</div></div></span>',
|
135
135
|
);
|
136
136
|
});
|
137
137
|
});
|
@@ -30,7 +30,7 @@ export class UnsupportedNodeExtension extends NodeExtension {
|
|
30
30
|
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
|
31
31
|
return {
|
32
32
|
selectable: false,
|
33
|
-
draggable:
|
33
|
+
draggable: false,
|
34
34
|
atom: true,
|
35
35
|
inline: true,
|
36
36
|
...override,
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { useState, useCallback, FocusEvent, FocusEventHandler, RefObject, createRef } from 'react';
|
2
|
+
|
3
|
+
const useFocus = (
|
4
|
+
initialState: boolean,
|
5
|
+
): {
|
6
|
+
handleFocus: () => void;
|
7
|
+
handleBlur: FocusEventHandler<HTMLDivElement>;
|
8
|
+
isVisible: boolean;
|
9
|
+
wrapperRef: RefObject<HTMLDivElement>;
|
10
|
+
} => {
|
11
|
+
const wrapperRef = createRef<HTMLDivElement>();
|
12
|
+
const [isVisible, setIsVisible] = useState(initialState);
|
13
|
+
|
14
|
+
const handleFocus = useCallback(() => {
|
15
|
+
setIsVisible(true);
|
16
|
+
}, []);
|
17
|
+
|
18
|
+
const handleBlur: FocusEventHandler<HTMLDivElement> = (event: FocusEvent<HTMLDivElement>) => {
|
19
|
+
const isOutside = wrapperRef.current !== null && !wrapperRef.current.contains(event.relatedTarget as Node);
|
20
|
+
if (isOutside) {
|
21
|
+
setIsVisible(false);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
|
25
|
+
return { handleFocus, handleBlur, isVisible, wrapperRef };
|
26
|
+
};
|
27
|
+
|
28
|
+
export default useFocus;
|
package/src/ui/_typography.scss
CHANGED
@@ -1,72 +1,74 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
.remirror-editor {
|
2
|
+
a {
|
3
|
+
@apply text-blue-300;
|
4
|
+
text-decoration: underline;
|
5
|
+
}
|
5
6
|
|
6
|
-
h1 {
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
}
|
7
|
+
h1 {
|
8
|
+
font-size: 1.625rem;
|
9
|
+
font-weight: 600;
|
10
|
+
letter-spacing: -0.2px;
|
11
|
+
line-height: 2rem;
|
12
|
+
}
|
12
13
|
|
13
|
-
h2 {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
}
|
14
|
+
h2 {
|
15
|
+
font-size: 1.25rem;
|
16
|
+
font-weight: 600;
|
17
|
+
letter-spacing: -0.5px;
|
18
|
+
line-height: 1.5rem;
|
19
|
+
}
|
19
20
|
|
20
|
-
h3 {
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
}
|
21
|
+
h3 {
|
22
|
+
font-size: 1.125rem;
|
23
|
+
font-weight: 600;
|
24
|
+
letter-spacing: -0.2px;
|
25
|
+
line-height: 1.375rem;
|
26
|
+
}
|
26
27
|
|
27
|
-
h4 {
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
}
|
28
|
+
h4 {
|
29
|
+
font-size: 1rem;
|
30
|
+
font-weight: 700;
|
31
|
+
letter-spacing: -0.2px;
|
32
|
+
line-height: 1.25rem;
|
33
|
+
}
|
33
34
|
|
34
|
-
h5 {
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
}
|
35
|
+
h5 {
|
36
|
+
font-size: 1rem;
|
37
|
+
font-weight: 600;
|
38
|
+
letter-spacing: -0.2px;
|
39
|
+
line-height: 1.25rem;
|
40
|
+
}
|
40
41
|
|
41
|
-
h6 {
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
}
|
42
|
+
h6 {
|
43
|
+
font-size: 0.875rem;
|
44
|
+
font-weight: 600;
|
45
|
+
letter-spacing: -0.2px;
|
46
|
+
line-height: 1.25rem;
|
47
|
+
}
|
47
48
|
|
48
|
-
.code-block,
|
49
|
-
.preformatted {
|
50
|
-
|
49
|
+
.code-block,
|
50
|
+
.preformatted {
|
51
|
+
display: flex;
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
53
|
+
.material-symbols-rounded {
|
54
|
+
@apply text-gray-700 text-xlg;
|
55
|
+
pointer-events: none;
|
56
|
+
margin-right: 0.375rem;
|
57
|
+
}
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
.block-divider {
|
60
|
+
@apply bg-gray-200;
|
61
|
+
width: 4px;
|
62
|
+
border-radius: 0.75rem;
|
63
|
+
margin-right: 0.625rem;
|
64
|
+
}
|
64
65
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
66
|
+
code,
|
67
|
+
pre {
|
68
|
+
@apply text-gray-700;
|
69
|
+
font-size: 0.75rem;
|
70
|
+
padding-top: 0.75rem;
|
71
|
+
padding-bottom: 0.75rem;
|
72
|
+
}
|
71
73
|
}
|
72
74
|
}
|