@gcforms/editor 0.0.1 → 1.0.1
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/CHANGELOG.md +21 -0
- package/package.json +5 -5
- package/src/Editor.tsx +86 -74
- package/src/config.ts +6 -3
- package/src/context/LocaleContext.tsx +31 -0
- package/src/context/ToolbarContext.tsx +86 -0
- package/src/hooks/useTranslation.tsx +19 -0
- package/src/i18n/en.json +22 -0
- package/src/i18n/fr.json +22 -0
- package/src/i18n/index.tsx +12 -0
- package/src/icons/CancelIcon.tsx +15 -0
- package/src/icons/CheckIcon.tsx +15 -0
- package/src/icons/DeleteIcon.tsx +15 -0
- package/src/icons/EditIcon.tsx +4 -3
- package/src/icons/LinkIcon.tsx +2 -2
- package/src/plugins/FloatingLinkEditorPlugin/index.css +50 -54
- package/src/plugins/FloatingLinkEditorPlugin/index.tsx +305 -138
- package/src/plugins/MaxLengthPlugin/index.tsx +73 -0
- package/src/plugins/MaxLengthPlugin/styles.css +7 -0
- package/src/plugins/ShortcutsPlugin/index.tsx +78 -0
- package/src/plugins/ShortcutsPlugin/shortcuts.ts +76 -0
- package/src/plugins/ToolbarPlugin/index.tsx +121 -149
- package/src/plugins/ToolbarPlugin/styles.css +4 -6
- package/src/plugins/ToolbarPlugin/utils.ts +172 -0
- package/src/styles.css +0 -8
- package/src/tests/Editor.cy.tsx +5 -3
- package/src/transformers/index.ts +18 -0
- package/src/ui/ContentEditable.css +2 -16
- package/src/ui/ContentEditable.tsx +2 -2
- package/src/ui/ToolTip.css +35 -0
- package/src/{ToolTip.tsx → ui/ToolTip.tsx} +4 -6
- package/src/utils/url.ts +7 -9
- package/src/themes/BasicTheme.ts +0 -69
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
10
|
+
import { HeadingTagType } from "@lexical/rich-text";
|
|
11
|
+
import { COMMAND_PRIORITY_NORMAL, KEY_MODIFIER_COMMAND } from "lexical";
|
|
12
|
+
import { Dispatch, useEffect } from "react";
|
|
13
|
+
|
|
14
|
+
import { useToolbarState } from "../../context/ToolbarContext";
|
|
15
|
+
import {
|
|
16
|
+
formatBulletList,
|
|
17
|
+
formatHeading,
|
|
18
|
+
formatNumberedList,
|
|
19
|
+
formatParagraph,
|
|
20
|
+
} from "../ToolbarPlugin/utils";
|
|
21
|
+
import {
|
|
22
|
+
isFormatBulletList,
|
|
23
|
+
isFormatHeading,
|
|
24
|
+
isFormatNumberedList,
|
|
25
|
+
isFormatParagraph,
|
|
26
|
+
isInsertLink,
|
|
27
|
+
} from "./shortcuts";
|
|
28
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
29
|
+
|
|
30
|
+
export default function ShortcutsPlugin({
|
|
31
|
+
setIsLinkEditMode,
|
|
32
|
+
}: {
|
|
33
|
+
setIsLinkEditMode: Dispatch<boolean>;
|
|
34
|
+
}): null {
|
|
35
|
+
const [editor] = useLexicalComposerContext();
|
|
36
|
+
const { toolbarState } = useToolbarState();
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const keyboardShortcutsHandler = (payload: KeyboardEvent) => {
|
|
40
|
+
const event: KeyboardEvent = payload;
|
|
41
|
+
|
|
42
|
+
if (isFormatParagraph(event)) {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
formatParagraph(editor);
|
|
45
|
+
} else if (isFormatHeading(event)) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
if (!["2", "3"].includes(event.code[event.code.length - 1])) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const { code } = event;
|
|
51
|
+
const headingSize = `h${code[code.length - 1]}` as HeadingTagType;
|
|
52
|
+
formatHeading(editor, toolbarState.blockType, headingSize);
|
|
53
|
+
} else if (isFormatBulletList(event)) {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
formatBulletList(editor, toolbarState.blockType);
|
|
56
|
+
} else if (isFormatNumberedList(event)) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
formatNumberedList(editor, toolbarState.blockType);
|
|
59
|
+
} else if (isInsertLink(event)) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
const url = toolbarState.isLink ? null : "";
|
|
62
|
+
setIsLinkEditMode(!toolbarState.isLink);
|
|
63
|
+
|
|
64
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return editor.registerCommand(
|
|
71
|
+
KEY_MODIFIER_COMMAND,
|
|
72
|
+
keyboardShortcutsHandler,
|
|
73
|
+
COMMAND_PRIORITY_NORMAL
|
|
74
|
+
);
|
|
75
|
+
}, [editor, toolbarState.isLink, toolbarState.blockType, setIsLinkEditMode]);
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { IS_APPLE } from "@lexical/utils";
|
|
10
|
+
|
|
11
|
+
//disable eslint sorting rule for quick reference to shortcuts
|
|
12
|
+
export const SHORTCUTS = Object.freeze({
|
|
13
|
+
// (Ctrl|⌘) + (Alt|Option) + <key> shortcuts
|
|
14
|
+
NORMAL: IS_APPLE ? "⌘+Opt+0" : "Ctrl+Alt+0",
|
|
15
|
+
HEADING2: IS_APPLE ? "⌘+Opt+2" : "Ctrl+Alt+2",
|
|
16
|
+
HEADING3: IS_APPLE ? "⌘+Opt+3" : "Ctrl+Alt+3",
|
|
17
|
+
BULLET_LIST: IS_APPLE ? "⌘+Opt+4" : "Ctrl+Alt+4",
|
|
18
|
+
NUMBERED_LIST: IS_APPLE ? "⌘+Opt+5" : "Ctrl+Alt+5",
|
|
19
|
+
|
|
20
|
+
// (Ctrl|⌘) + Shift + <key> shortcuts
|
|
21
|
+
//
|
|
22
|
+
|
|
23
|
+
// (Ctrl|⌘) + <key> shortcuts
|
|
24
|
+
BOLD: IS_APPLE ? "⌘+B" : "Ctrl+B",
|
|
25
|
+
ITALIC: IS_APPLE ? "⌘+I" : "Ctrl+I",
|
|
26
|
+
INSERT_LINK: IS_APPLE ? "⌘+K" : "Ctrl+K",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function controlOrMeta(metaKey: boolean, ctrlKey: boolean): boolean {
|
|
30
|
+
return IS_APPLE ? metaKey : ctrlKey;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isFormatParagraph(event: KeyboardEvent): boolean {
|
|
34
|
+
const { code, shiftKey, altKey, metaKey, ctrlKey } = event;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
(code === "Numpad0" || code === "Digit0") &&
|
|
38
|
+
!shiftKey &&
|
|
39
|
+
altKey &&
|
|
40
|
+
controlOrMeta(metaKey, ctrlKey)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isFormatHeading(event: KeyboardEvent): boolean {
|
|
45
|
+
const { code, shiftKey, altKey, metaKey, ctrlKey } = event;
|
|
46
|
+
const keyNumber = code[code.length - 1];
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
["1", "2", "3"].includes(keyNumber) && !shiftKey && altKey && controlOrMeta(metaKey, ctrlKey)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isFormatBulletList(event: KeyboardEvent): boolean {
|
|
54
|
+
const { code, shiftKey, altKey, metaKey, ctrlKey } = event;
|
|
55
|
+
return (
|
|
56
|
+
(code === "Numpad4" || code === "Digit4") &&
|
|
57
|
+
!shiftKey &&
|
|
58
|
+
altKey &&
|
|
59
|
+
controlOrMeta(metaKey, ctrlKey)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isFormatNumberedList(event: KeyboardEvent): boolean {
|
|
64
|
+
const { code, shiftKey, altKey, metaKey, ctrlKey } = event;
|
|
65
|
+
return (
|
|
66
|
+
(code === "Numpad5" || code === "Digit5") &&
|
|
67
|
+
!shiftKey &&
|
|
68
|
+
altKey &&
|
|
69
|
+
controlOrMeta(metaKey, ctrlKey)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isInsertLink(event: KeyboardEvent): boolean {
|
|
74
|
+
const { code, shiftKey, altKey, metaKey, ctrlKey } = event;
|
|
75
|
+
return code === "KeyK" && !shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey);
|
|
76
|
+
}
|
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React, { useState, useCallback, useEffect, useRef, KeyboardEvent } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import { $
|
|
5
|
-
import { mergeRegister, $getNearestNodeOfType } from "@lexical/utils";
|
|
3
|
+
import { $isHeadingNode } from "@lexical/rich-text";
|
|
4
|
+
import { mergeRegister, $getNearestNodeOfType, $findMatchingParent } from "@lexical/utils";
|
|
6
5
|
|
|
7
6
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
import { useTranslation } from "@i18n/client";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
$isListNode,
|
|
14
|
-
INSERT_ORDERED_LIST_COMMAND,
|
|
15
|
-
INSERT_UNORDERED_LIST_COMMAND,
|
|
16
|
-
REMOVE_LIST_COMMAND,
|
|
17
|
-
ListNode,
|
|
18
|
-
} from "@lexical/list";
|
|
8
|
+
import { $isListNode, ListNode } from "@lexical/list";
|
|
19
9
|
|
|
20
10
|
import {
|
|
21
11
|
FORMAT_TEXT_COMMAND,
|
|
22
12
|
$getSelection,
|
|
23
13
|
$isRangeSelection,
|
|
24
14
|
SELECTION_CHANGE_COMMAND,
|
|
25
|
-
$
|
|
15
|
+
$isRootOrShadowRoot,
|
|
16
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
17
|
+
CAN_UNDO_COMMAND,
|
|
18
|
+
CAN_REDO_COMMAND,
|
|
26
19
|
} from "lexical";
|
|
27
20
|
|
|
28
|
-
import { $wrapNodes } from "@lexical/selection";
|
|
29
|
-
import { sanitizeUrl } from "../../utils/url";
|
|
30
21
|
import { useEditorFocus } from "../../hooks/useEditorFocus";
|
|
31
22
|
import { getSelectedNode } from "../../utils/getSelectedNode";
|
|
32
|
-
import { ToolTip } from "../../ToolTip";
|
|
23
|
+
import { ToolTip } from "../../ui/ToolTip";
|
|
33
24
|
import { H2Icon } from "../../icons/H2Icon";
|
|
34
25
|
import { H3Icon } from "../../icons/H3Icon";
|
|
35
26
|
import { BoldIcon } from "../../icons/BoldIcon";
|
|
@@ -37,43 +28,38 @@ import { ItalicIcon } from "../../icons/ItalicIcon";
|
|
|
37
28
|
import { BulletListIcon } from "../../icons/BulletListIcon";
|
|
38
29
|
import { NumberedListIcon } from "../../icons/NumberedListIcon";
|
|
39
30
|
import { LinkIcon } from "../../icons/LinkIcon";
|
|
31
|
+
import { useTranslation } from "../../hooks/useTranslation";
|
|
32
|
+
import { formatBulletList, formatHeading, formatNumberedList } from "./utils";
|
|
33
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
40
34
|
import "./styles.css";
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
h6: "Heading 6",
|
|
52
|
-
number: "Numbered List",
|
|
53
|
-
paragraph: "Normal",
|
|
54
|
-
quote: "Quote",
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const LowPriority = 1;
|
|
58
|
-
type HeadingTagType = "h2" | "h3" | "h4" | "h5";
|
|
59
|
-
|
|
60
|
-
export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
35
|
+
import { SHORTCUTS } from "../ShortcutsPlugin/shortcuts";
|
|
36
|
+
import { blockTypeToBlockName, useToolbarState } from "../../context/ToolbarContext";
|
|
37
|
+
|
|
38
|
+
export default function ToolbarPlugin({
|
|
39
|
+
editorId,
|
|
40
|
+
setIsLinkEditMode,
|
|
41
|
+
}: {
|
|
42
|
+
editorId: string;
|
|
43
|
+
setIsLinkEditMode: (isLinkEditMode: boolean) => void;
|
|
44
|
+
}) {
|
|
61
45
|
const [editor] = useLexicalComposerContext();
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const [isLink, setIsLink] = useState(false);
|
|
46
|
+
|
|
47
|
+
const { toolbarState, updateToolbarState } = useToolbarState();
|
|
65
48
|
const [, setSelectedElementKey] = useState("");
|
|
66
|
-
const [
|
|
49
|
+
const [isEditable, setIsEditable] = useState(() => editor.isEditable());
|
|
50
|
+
const [activeEditor, setActiveEditor] = useState(editor);
|
|
67
51
|
|
|
68
|
-
const
|
|
52
|
+
const { t } = useTranslation();
|
|
69
53
|
|
|
70
54
|
const insertLink = useCallback(() => {
|
|
71
|
-
if (!isLink) {
|
|
72
|
-
|
|
55
|
+
if (!toolbarState.isLink) {
|
|
56
|
+
setIsLinkEditMode(true);
|
|
57
|
+
activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, "");
|
|
73
58
|
} else {
|
|
74
|
-
|
|
59
|
+
setIsLinkEditMode(false);
|
|
60
|
+
activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
75
61
|
}
|
|
76
|
-
}, [
|
|
62
|
+
}, [activeEditor, setIsLinkEditMode, toolbarState.isLink]);
|
|
77
63
|
|
|
78
64
|
const [items] = useState([
|
|
79
65
|
{ id: 1, txt: "heading2" },
|
|
@@ -117,117 +103,101 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
117
103
|
[items, setCurrentFocusIndex, setToolbarInit, toolbarInit]
|
|
118
104
|
);
|
|
119
105
|
|
|
120
|
-
const
|
|
121
|
-
if (blockType === level) {
|
|
122
|
-
formatParagraph();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (blockType !== level) {
|
|
126
|
-
editor.update(() => {
|
|
127
|
-
const selection = $getSelection();
|
|
128
|
-
if ($isRangeSelection(selection)) {
|
|
129
|
-
$wrapNodes(selection, () => $createHeadingNode(level));
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const formatParagraph = () => {
|
|
136
|
-
if (blockType !== "paragraph") {
|
|
137
|
-
editor.update(() => {
|
|
138
|
-
const selection = $getSelection();
|
|
139
|
-
|
|
140
|
-
if ($isRangeSelection(selection)) {
|
|
141
|
-
$wrapNodes(selection, () => $createParagraphNode());
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const formatBulletList = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
|
148
|
-
evt.preventDefault();
|
|
149
|
-
if (blockType !== "bullet") {
|
|
150
|
-
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const formatNumberedList = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
|
157
|
-
evt.preventDefault();
|
|
158
|
-
if (blockType !== "number") {
|
|
159
|
-
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const updateToolbar = useCallback(() => {
|
|
106
|
+
const $updateToolbar = useCallback(() => {
|
|
166
107
|
const selection = $getSelection();
|
|
167
108
|
|
|
168
109
|
if ($isRangeSelection(selection)) {
|
|
169
110
|
const anchorNode = selection.anchor.getNode();
|
|
170
|
-
|
|
171
|
-
anchorNode.getKey() === "root"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
111
|
+
let element =
|
|
112
|
+
anchorNode.getKey() === "root"
|
|
113
|
+
? anchorNode
|
|
114
|
+
: $findMatchingParent(anchorNode, (e) => {
|
|
115
|
+
const parent = e.getParent();
|
|
116
|
+
return parent !== null && $isRootOrShadowRoot(parent);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (element === null) {
|
|
120
|
+
element = anchorNode.getTopLevelElementOrThrow();
|
|
179
121
|
}
|
|
180
122
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
setIsItalic(selection.hasFormat("italic"));
|
|
123
|
+
const elementKey = element.getKey();
|
|
124
|
+
const elementDOM = editor.getElementByKey(elementKey);
|
|
184
125
|
|
|
185
|
-
//
|
|
126
|
+
// Update links
|
|
186
127
|
const node = getSelectedNode(selection);
|
|
187
128
|
const parent = node.getParent();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
191
|
-
setIsLink(true);
|
|
192
|
-
} else {
|
|
193
|
-
setIsLink(false);
|
|
194
|
-
}
|
|
129
|
+
const isLink = $isLinkNode(parent) || $isLinkNode(node);
|
|
130
|
+
updateToolbarState("isLink", isLink);
|
|
195
131
|
|
|
196
132
|
if (elementDOM !== null) {
|
|
197
133
|
setSelectedElementKey(elementKey);
|
|
198
134
|
if ($isListNode(element)) {
|
|
199
135
|
const parentList = $getNearestNodeOfType<ListNode>(anchorNode, ListNode);
|
|
200
136
|
const type = parentList ? parentList.getListType() : element.getListType();
|
|
201
|
-
|
|
137
|
+
|
|
138
|
+
updateToolbarState("blockType", type);
|
|
202
139
|
} else {
|
|
203
140
|
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
|
|
204
141
|
if (type in blockTypeToBlockName) {
|
|
205
|
-
|
|
142
|
+
updateToolbarState("blockType", type as keyof typeof blockTypeToBlockName);
|
|
206
143
|
}
|
|
207
144
|
}
|
|
208
145
|
}
|
|
209
146
|
}
|
|
210
|
-
|
|
147
|
+
if ($isRangeSelection(selection)) {
|
|
148
|
+
// Update text format
|
|
149
|
+
updateToolbarState("isBold", selection.hasFormat("bold"));
|
|
150
|
+
updateToolbarState("isItalic", selection.hasFormat("italic"));
|
|
151
|
+
}
|
|
152
|
+
}, [editor, updateToolbarState]);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
return editor.registerCommand(
|
|
156
|
+
SELECTION_CHANGE_COMMAND,
|
|
157
|
+
(_payload, newEditor) => {
|
|
158
|
+
setActiveEditor(newEditor);
|
|
159
|
+
$updateToolbar();
|
|
160
|
+
return false;
|
|
161
|
+
},
|
|
162
|
+
COMMAND_PRIORITY_CRITICAL
|
|
163
|
+
);
|
|
164
|
+
}, [editor, $updateToolbar, setActiveEditor]);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
activeEditor.getEditorState().read(() => {
|
|
168
|
+
$updateToolbar();
|
|
169
|
+
});
|
|
170
|
+
}, [activeEditor, $updateToolbar]);
|
|
211
171
|
|
|
212
172
|
useEffect(() => {
|
|
213
173
|
return mergeRegister(
|
|
214
|
-
editor.
|
|
174
|
+
editor.registerEditableListener((editable) => {
|
|
175
|
+
setIsEditable(editable);
|
|
176
|
+
}),
|
|
177
|
+
activeEditor.registerUpdateListener(({ editorState }) => {
|
|
215
178
|
editorState.read(() => {
|
|
216
|
-
updateToolbar();
|
|
179
|
+
$updateToolbar();
|
|
217
180
|
});
|
|
218
181
|
}),
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
() => {
|
|
222
|
-
|
|
182
|
+
activeEditor.registerCommand<boolean>(
|
|
183
|
+
CAN_UNDO_COMMAND,
|
|
184
|
+
(payload) => {
|
|
185
|
+
updateToolbarState("canUndo", payload);
|
|
186
|
+
return false;
|
|
187
|
+
},
|
|
188
|
+
COMMAND_PRIORITY_CRITICAL
|
|
189
|
+
),
|
|
190
|
+
activeEditor.registerCommand<boolean>(
|
|
191
|
+
CAN_REDO_COMMAND,
|
|
192
|
+
(payload) => {
|
|
193
|
+
updateToolbarState("canRedo", payload);
|
|
223
194
|
return false;
|
|
224
195
|
},
|
|
225
|
-
|
|
196
|
+
COMMAND_PRIORITY_CRITICAL
|
|
226
197
|
)
|
|
227
198
|
);
|
|
228
|
-
}, [editor,
|
|
199
|
+
}, [$updateToolbar, activeEditor, editor, updateToolbarState]);
|
|
229
200
|
|
|
230
|
-
const { t } = useTranslation("form-builder");
|
|
231
201
|
const editorHasFocus = useEditorFocus();
|
|
232
202
|
|
|
233
203
|
return (
|
|
@@ -240,7 +210,7 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
240
210
|
onKeyDown={handleNav}
|
|
241
211
|
data-testid="toolbar"
|
|
242
212
|
>
|
|
243
|
-
<ToolTip text={t("tooltipFormatH2")}>
|
|
213
|
+
<ToolTip text={t("tooltipFormatH2") + ` (${SHORTCUTS.HEADING2})`}>
|
|
244
214
|
<button
|
|
245
215
|
tabIndex={currentFocusIndex == 0 ? 0 : -1}
|
|
246
216
|
ref={(el) => {
|
|
@@ -250,20 +220,20 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
250
220
|
}
|
|
251
221
|
}}
|
|
252
222
|
onClick={() => {
|
|
253
|
-
formatHeading("h2");
|
|
223
|
+
formatHeading(editor, toolbarState.blockType, "h2");
|
|
254
224
|
}}
|
|
255
225
|
className={
|
|
256
|
-
"toolbar-item
|
|
226
|
+
"toolbar-item " + (toolbarState.blockType === "h2" && editorHasFocus ? "active" : "")
|
|
257
227
|
}
|
|
258
228
|
aria-label={t("formatH2")}
|
|
259
|
-
aria-pressed={blockType === "h2"}
|
|
229
|
+
aria-pressed={toolbarState.blockType === "h2"}
|
|
260
230
|
data-testid={`h2-button`}
|
|
261
231
|
>
|
|
262
232
|
<H2Icon />
|
|
263
233
|
</button>
|
|
264
234
|
</ToolTip>
|
|
265
235
|
|
|
266
|
-
<ToolTip text={t("tooltipFormatH3")}>
|
|
236
|
+
<ToolTip text={t("tooltipFormatH3") + ` (${SHORTCUTS.HEADING3})`}>
|
|
267
237
|
<button
|
|
268
238
|
tabIndex={currentFocusIndex == 1 ? 0 : -1}
|
|
269
239
|
ref={(el) => {
|
|
@@ -273,20 +243,20 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
273
243
|
}
|
|
274
244
|
}}
|
|
275
245
|
onClick={() => {
|
|
276
|
-
formatHeading("h3");
|
|
246
|
+
formatHeading(editor, toolbarState.blockType, "h3");
|
|
277
247
|
}}
|
|
278
248
|
className={
|
|
279
|
-
"
|
|
249
|
+
"toolbar-item " + (toolbarState.blockType === "h3" && editorHasFocus ? "active" : "")
|
|
280
250
|
}
|
|
281
251
|
aria-label={t("formatH3")}
|
|
282
|
-
aria-pressed={blockType === "h3"}
|
|
252
|
+
aria-pressed={toolbarState.blockType === "h3"}
|
|
283
253
|
data-testid={`h3-button`}
|
|
284
254
|
>
|
|
285
255
|
<H3Icon />
|
|
286
256
|
</button>
|
|
287
257
|
</ToolTip>
|
|
288
258
|
|
|
289
|
-
<ToolTip text={t("tooltipFormatBold")}>
|
|
259
|
+
<ToolTip text={t("tooltipFormatBold") + ` (${SHORTCUTS.BOLD})`}>
|
|
290
260
|
<button
|
|
291
261
|
tabIndex={currentFocusIndex == 2 ? 0 : -1}
|
|
292
262
|
ref={(el) => {
|
|
@@ -298,16 +268,16 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
298
268
|
onClick={() => {
|
|
299
269
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
300
270
|
}}
|
|
301
|
-
className={"
|
|
271
|
+
className={"toolbar-item " + (toolbarState.isBold && editorHasFocus ? "active" : "")}
|
|
302
272
|
aria-label={t("formatBold")}
|
|
303
|
-
aria-pressed={isBold}
|
|
273
|
+
aria-pressed={toolbarState.isBold}
|
|
304
274
|
data-testid={`bold-button`}
|
|
305
275
|
>
|
|
306
276
|
<BoldIcon />
|
|
307
277
|
</button>
|
|
308
278
|
</ToolTip>
|
|
309
279
|
|
|
310
|
-
<ToolTip text={t("tooltipFormatItalic")}>
|
|
280
|
+
<ToolTip text={t("tooltipFormatItalic") + ` (${SHORTCUTS.ITALIC})`}>
|
|
311
281
|
<button
|
|
312
282
|
tabIndex={currentFocusIndex == 3 ? 0 : -1}
|
|
313
283
|
ref={(el) => {
|
|
@@ -319,16 +289,16 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
319
289
|
onClick={() => {
|
|
320
290
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
|
|
321
291
|
}}
|
|
322
|
-
className={"
|
|
292
|
+
className={"toolbar-item " + (toolbarState.isItalic && editorHasFocus ? "active" : "")}
|
|
323
293
|
aria-label={t("formatItalic")}
|
|
324
|
-
aria-pressed={isItalic}
|
|
294
|
+
aria-pressed={toolbarState.isItalic}
|
|
325
295
|
data-testid={`italic-button`}
|
|
326
296
|
>
|
|
327
297
|
<ItalicIcon />
|
|
328
298
|
</button>
|
|
329
299
|
</ToolTip>
|
|
330
300
|
|
|
331
|
-
<ToolTip text={t("tooltipFormatBulletList")}>
|
|
301
|
+
<ToolTip text={t("tooltipFormatBulletList") + ` (${SHORTCUTS.BULLET_LIST})`}>
|
|
332
302
|
<button
|
|
333
303
|
tabIndex={currentFocusIndex == 4 ? 0 : -1}
|
|
334
304
|
ref={(el) => {
|
|
@@ -337,19 +307,20 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
337
307
|
itemsRef.current[index] = el;
|
|
338
308
|
}
|
|
339
309
|
}}
|
|
340
|
-
onClick={formatBulletList}
|
|
310
|
+
onClick={() => formatBulletList(editor, toolbarState.blockType)}
|
|
341
311
|
className={
|
|
342
|
-
"
|
|
312
|
+
"toolbar-item " +
|
|
313
|
+
(toolbarState.blockType === "bullet" && editorHasFocus ? "active" : "")
|
|
343
314
|
}
|
|
344
315
|
aria-label={t("formatBulletList")}
|
|
345
|
-
aria-pressed={blockType === "bullet"}
|
|
316
|
+
aria-pressed={toolbarState.blockType === "bullet"}
|
|
346
317
|
data-testid={`bullet-list-button`}
|
|
347
318
|
>
|
|
348
319
|
<BulletListIcon />
|
|
349
320
|
</button>
|
|
350
321
|
</ToolTip>
|
|
351
322
|
|
|
352
|
-
<ToolTip text={t("tooltipFormatNumberedList")}>
|
|
323
|
+
<ToolTip text={t("tooltipFormatNumberedList") + ` (${SHORTCUTS.NUMBERED_LIST})`}>
|
|
353
324
|
<button
|
|
354
325
|
tabIndex={currentFocusIndex == 5 ? 0 : -1}
|
|
355
326
|
ref={(el) => {
|
|
@@ -358,19 +329,20 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
358
329
|
itemsRef.current[index] = el;
|
|
359
330
|
}
|
|
360
331
|
}}
|
|
361
|
-
onClick={formatNumberedList}
|
|
332
|
+
onClick={() => formatNumberedList(editor, toolbarState.blockType)}
|
|
362
333
|
className={
|
|
363
|
-
"
|
|
334
|
+
"toolbar-item " +
|
|
335
|
+
(toolbarState.blockType === "number" && editorHasFocus ? "active" : "")
|
|
364
336
|
}
|
|
365
337
|
aria-label={t("formatNumberedList")}
|
|
366
|
-
aria-pressed={blockType === "number"}
|
|
338
|
+
aria-pressed={toolbarState.blockType === "number"}
|
|
367
339
|
data-testid={`numbered-list-button`}
|
|
368
340
|
>
|
|
369
341
|
<NumberedListIcon />
|
|
370
342
|
</button>
|
|
371
343
|
</ToolTip>
|
|
372
344
|
|
|
373
|
-
<ToolTip text={t("tooltipInsertLink")}>
|
|
345
|
+
<ToolTip text={t("tooltipInsertLink") + ` (${SHORTCUTS.INSERT_LINK})`}>
|
|
374
346
|
<button
|
|
375
347
|
tabIndex={currentFocusIndex == 6 ? 0 : -1}
|
|
376
348
|
ref={(el) => {
|
|
@@ -381,9 +353,9 @@ export default function ToolbarPlugin({ editorId }: { editorId: string }) {
|
|
|
381
353
|
}}
|
|
382
354
|
disabled={!isEditable}
|
|
383
355
|
onClick={insertLink}
|
|
384
|
-
className={"
|
|
356
|
+
className={"toolbar-item " + (toolbarState.isLink && editorHasFocus ? "active" : "")}
|
|
385
357
|
aria-label={t("insertLink")}
|
|
386
|
-
aria-pressed={isLink}
|
|
358
|
+
aria-pressed={toolbarState.isLink}
|
|
387
359
|
data-testid={`link-button`}
|
|
388
360
|
>
|
|
389
361
|
<LinkIcon />
|
|
@@ -3,20 +3,18 @@
|
|
|
3
3
|
border-radius: 2px 2px 0 0;
|
|
4
4
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
|
5
5
|
border-bottom: 1px solid #e9ecef;
|
|
6
|
+
display: flex;
|
|
7
|
+
gap: 6px;
|
|
6
8
|
|
|
7
9
|
&:focus-within {
|
|
8
10
|
margin: -2px;
|
|
9
11
|
border: 2px solid #015ecc;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
button {
|
|
13
|
-
padding:
|
|
14
|
+
button.toolbar-item {
|
|
15
|
+
padding: 2px 5px 5px 2px;
|
|
14
16
|
border: 1.5px solid transparent;
|
|
15
17
|
border-radius: 4px;
|
|
16
|
-
margin-right: 5px;
|
|
17
|
-
svg {
|
|
18
|
-
display: block;
|
|
19
|
-
}
|
|
20
18
|
&.active {
|
|
21
19
|
border: 1.5px solid #015ecc;
|
|
22
20
|
}
|