@eventuras/scribo 0.4.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/LICENSE.md +31 -0
- package/README.md +31 -0
- package/eslint.config.js +4 -0
- package/index.html +13 -0
- package/package.json +41 -0
- package/src/App.css +35 -0
- package/src/App.tsx +42 -0
- package/src/MarkdownEditor.css +1127 -0
- package/src/MarkdownEditor.tsx +105 -0
- package/src/context/SharedHistoryContext.tsx +36 -0
- package/src/index.tsx +12 -0
- package/src/main.tsx +4 -0
- package/src/nodes/EditorNodes.ts +39 -0
- package/src/plugins/AutoLinkPlugin.tsx +34 -0
- package/src/plugins/FloatingLinkEditorPlugin.css +41 -0
- package/src/plugins/FloatingLinkEditorPlugin.tsx +469 -0
- package/src/plugins/HistoryPlugin.tsx +506 -0
- package/src/plugins/LinkPlugin.tsx +17 -0
- package/src/plugins/ToolbarPlugin.tsx +528 -0
- package/src/themes/EditorTheme.css +297 -0
- package/src/themes/EditorTheme.ts +118 -0
- package/src/ui/ContentEditable.css +23 -0
- package/src/ui/ContentEditable.tsx +19 -0
- package/src/ui/DropDown.tsx +262 -0
- package/src/ui/Placeholder.css +28 -0
- package/src/ui/Placeholder.tsx +21 -0
- package/src/utils/environment.ts +54 -0
- package/src/utils/getSelectedNode.ts +28 -0
- package/src/utils/setFloatingElemPosition.ts +51 -0
- package/src/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
- package/src/utils/url.ts +39 -0
- package/src/vite-env.d.ts +8 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modified from the the work licensed under the MIT license below:
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* Source: https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
$createCodeNode,
|
|
14
|
+
$isCodeNode,
|
|
15
|
+
CODE_LANGUAGE_FRIENDLY_NAME_MAP,
|
|
16
|
+
CODE_LANGUAGE_MAP,
|
|
17
|
+
getLanguageFriendlyName,
|
|
18
|
+
} from '@lexical/code';
|
|
19
|
+
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
20
|
+
import {
|
|
21
|
+
$isListNode,
|
|
22
|
+
INSERT_ORDERED_LIST_COMMAND,
|
|
23
|
+
INSERT_UNORDERED_LIST_COMMAND,
|
|
24
|
+
ListNode,
|
|
25
|
+
REMOVE_LIST_COMMAND,
|
|
26
|
+
} from "@lexical/list";
|
|
27
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
28
|
+
import {
|
|
29
|
+
$createHeadingNode,
|
|
30
|
+
$createQuoteNode,
|
|
31
|
+
$isHeadingNode,
|
|
32
|
+
HeadingTagType,
|
|
33
|
+
} from "@lexical/rich-text";
|
|
34
|
+
import { $setBlocksType } from "@lexical/selection";
|
|
35
|
+
import {
|
|
36
|
+
$findMatchingParent,
|
|
37
|
+
$getNearestNodeOfType,
|
|
38
|
+
mergeRegister,
|
|
39
|
+
} from "@lexical/utils";
|
|
40
|
+
import {
|
|
41
|
+
$createParagraphNode,
|
|
42
|
+
$getNodeByKey,
|
|
43
|
+
$getSelection,
|
|
44
|
+
$isRangeSelection,
|
|
45
|
+
$isRootOrShadowRoot,
|
|
46
|
+
CAN_REDO_COMMAND,
|
|
47
|
+
CAN_UNDO_COMMAND,
|
|
48
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
49
|
+
COMMAND_PRIORITY_NORMAL,
|
|
50
|
+
FORMAT_TEXT_COMMAND,
|
|
51
|
+
KEY_MODIFIER_COMMAND,
|
|
52
|
+
LexicalEditor,
|
|
53
|
+
NodeKey,
|
|
54
|
+
REDO_COMMAND,
|
|
55
|
+
SELECTION_CHANGE_COMMAND,
|
|
56
|
+
UNDO_COMMAND,
|
|
57
|
+
} from 'lexical';
|
|
58
|
+
import { Dispatch, useCallback, useEffect, useState } from "react";
|
|
59
|
+
import type {JSX} from 'react';
|
|
60
|
+
|
|
61
|
+
import DropDown, { DropDownItem } from "../ui/DropDown";
|
|
62
|
+
import { IS_APPLE } from "../utils/environment";
|
|
63
|
+
import { getSelectedNode } from "../utils/getSelectedNode";
|
|
64
|
+
import { sanitizeUrl } from "../utils/url";
|
|
65
|
+
|
|
66
|
+
const blockTypeToBlockName = {
|
|
67
|
+
paragraph: "Normal",
|
|
68
|
+
h1: "Heading 1",
|
|
69
|
+
h2: "Heading 2",
|
|
70
|
+
h3: "Heading 3",
|
|
71
|
+
h4: "Heading 4",
|
|
72
|
+
bullet: "Bulleted List",
|
|
73
|
+
check: "Check List",
|
|
74
|
+
code: "Code Block",
|
|
75
|
+
number: "Numbered List",
|
|
76
|
+
quote: "Quote",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function getCodeLanguageOptions(): [string, string][] {
|
|
80
|
+
const options: [string, string][] = [];
|
|
81
|
+
|
|
82
|
+
for (const [lang, friendlyName] of Object.entries(
|
|
83
|
+
CODE_LANGUAGE_FRIENDLY_NAME_MAP,
|
|
84
|
+
)) {
|
|
85
|
+
options.push([lang, friendlyName]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return options;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const CODE_LANGUAGE_OPTIONS = getCodeLanguageOptions();
|
|
92
|
+
|
|
93
|
+
function dropDownActiveClass(active: boolean) {
|
|
94
|
+
if (active) return "active dropdown-item-active";
|
|
95
|
+
else return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function BlockFormatDropDown({
|
|
99
|
+
editor,
|
|
100
|
+
blockType,
|
|
101
|
+
disabled = false,
|
|
102
|
+
}: {
|
|
103
|
+
blockType: keyof typeof blockTypeToBlockName;
|
|
104
|
+
editor: LexicalEditor;
|
|
105
|
+
disabled?: boolean;
|
|
106
|
+
}): JSX.Element {
|
|
107
|
+
const formatParagraph = () => {
|
|
108
|
+
editor.update(() => {
|
|
109
|
+
const selection = $getSelection();
|
|
110
|
+
if ($isRangeSelection(selection)) {
|
|
111
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const formatHeading = (headingSize: HeadingTagType) => {
|
|
117
|
+
if (blockType !== headingSize) {
|
|
118
|
+
editor.update(() => {
|
|
119
|
+
const selection = $getSelection();
|
|
120
|
+
if ($isRangeSelection(selection)) {
|
|
121
|
+
$setBlocksType(selection, () => $createHeadingNode(headingSize));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const formatBulletList = () => {
|
|
128
|
+
if (blockType !== "bullet") {
|
|
129
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
130
|
+
} else {
|
|
131
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const formatNumberedList = () => {
|
|
136
|
+
if (blockType !== "number") {
|
|
137
|
+
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
138
|
+
} else {
|
|
139
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const formatQuote = () => {
|
|
144
|
+
if (blockType !== 'quote') {
|
|
145
|
+
editor.update(() => {
|
|
146
|
+
const selection = $getSelection();
|
|
147
|
+
if ($isRangeSelection(selection)) {
|
|
148
|
+
$setBlocksType(selection, () => $createQuoteNode());
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const formatCode = () => {
|
|
155
|
+
if (blockType !== 'code') {
|
|
156
|
+
editor.update(() => {
|
|
157
|
+
let selection = $getSelection();
|
|
158
|
+
|
|
159
|
+
if (selection !== null) {
|
|
160
|
+
if (selection.isCollapsed()) {
|
|
161
|
+
$setBlocksType(selection, () => $createCodeNode());
|
|
162
|
+
} else {
|
|
163
|
+
const textContent = selection.getTextContent();
|
|
164
|
+
const codeNode = $createCodeNode();
|
|
165
|
+
selection.insertNodes([codeNode]);
|
|
166
|
+
selection = $getSelection();
|
|
167
|
+
if ($isRangeSelection(selection)) {
|
|
168
|
+
selection.insertRawText(textContent);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<DropDown
|
|
178
|
+
disabled={disabled}
|
|
179
|
+
buttonClassName="toolbar-item block-controls"
|
|
180
|
+
buttonIconClassName={"icon block-type " + blockType}
|
|
181
|
+
buttonLabel={blockTypeToBlockName[blockType]}
|
|
182
|
+
buttonAriaLabel="Formatting options for text style"
|
|
183
|
+
>
|
|
184
|
+
<DropDownItem
|
|
185
|
+
className={"item " + dropDownActiveClass(blockType === "paragraph")}
|
|
186
|
+
onClick={formatParagraph}
|
|
187
|
+
>
|
|
188
|
+
<i className="icon paragraph" />
|
|
189
|
+
<span className="text">Normal</span>
|
|
190
|
+
</DropDownItem>
|
|
191
|
+
<DropDownItem
|
|
192
|
+
className={"item " + dropDownActiveClass(blockType === "h1")}
|
|
193
|
+
onClick={() => formatHeading("h1")}
|
|
194
|
+
>
|
|
195
|
+
<i className="icon h1" />
|
|
196
|
+
<span className="text">Heading 1</span>
|
|
197
|
+
</DropDownItem>
|
|
198
|
+
<DropDownItem
|
|
199
|
+
className={"item " + dropDownActiveClass(blockType === "h2")}
|
|
200
|
+
onClick={() => formatHeading("h2")}
|
|
201
|
+
>
|
|
202
|
+
<i className="icon h2" />
|
|
203
|
+
<span className="text">Heading 2</span>
|
|
204
|
+
</DropDownItem>
|
|
205
|
+
<DropDownItem
|
|
206
|
+
className={"item " + dropDownActiveClass(blockType === "h3")}
|
|
207
|
+
onClick={() => formatHeading("h3")}
|
|
208
|
+
>
|
|
209
|
+
<i className="icon h3" />
|
|
210
|
+
<span className="text">Heading 3</span>
|
|
211
|
+
</DropDownItem>
|
|
212
|
+
<DropDownItem
|
|
213
|
+
className={"item " + dropDownActiveClass(blockType === "h4")}
|
|
214
|
+
onClick={() => formatHeading("h4")}
|
|
215
|
+
>
|
|
216
|
+
<i className="icon h4" />
|
|
217
|
+
<span className="text">Heading 4</span>
|
|
218
|
+
</DropDownItem>
|
|
219
|
+
<DropDownItem
|
|
220
|
+
className={"item " + dropDownActiveClass(blockType === "bullet")}
|
|
221
|
+
onClick={formatBulletList}
|
|
222
|
+
>
|
|
223
|
+
<i className="icon bullet-list" />
|
|
224
|
+
<span className="text">Bullet List</span>
|
|
225
|
+
</DropDownItem>
|
|
226
|
+
<DropDownItem
|
|
227
|
+
className={"item " + dropDownActiveClass(blockType === "number")}
|
|
228
|
+
onClick={formatNumberedList}
|
|
229
|
+
>
|
|
230
|
+
<i className="icon numbered-list" />
|
|
231
|
+
<span className="text">Numbered List</span>
|
|
232
|
+
</DropDownItem>
|
|
233
|
+
<DropDownItem
|
|
234
|
+
className={"item " + dropDownActiveClass(blockType === "quote")}
|
|
235
|
+
onClick={formatQuote}
|
|
236
|
+
>
|
|
237
|
+
<i className="icon quote" />
|
|
238
|
+
<span className="text">Quote</span>
|
|
239
|
+
</DropDownItem>
|
|
240
|
+
<DropDownItem
|
|
241
|
+
className={'item ' + dropDownActiveClass(blockType === 'code')}
|
|
242
|
+
onClick={formatCode}>
|
|
243
|
+
<i className="icon code" />
|
|
244
|
+
<span className="text">Code Block</span>
|
|
245
|
+
</DropDownItem>
|
|
246
|
+
</DropDown>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function Divider(): JSX.Element {
|
|
251
|
+
return <div className="divider" />;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export default function ToolbarPlugin({
|
|
255
|
+
setIsLinkEditMode,
|
|
256
|
+
}: {
|
|
257
|
+
setIsLinkEditMode: Dispatch<boolean>;
|
|
258
|
+
}): JSX.Element {
|
|
259
|
+
const [editor] = useLexicalComposerContext();
|
|
260
|
+
const [activeEditor, setActiveEditor] = useState(editor);
|
|
261
|
+
const [blockType, setBlockType] =
|
|
262
|
+
useState<keyof typeof blockTypeToBlockName>("paragraph");
|
|
263
|
+
const [selectedElementKey, setSelectedElementKey] = useState<NodeKey | null>(
|
|
264
|
+
null,
|
|
265
|
+
);
|
|
266
|
+
const [isCode, setIsCode] = useState(false);
|
|
267
|
+
const [isLink, setIsLink] = useState(false);
|
|
268
|
+
const [isBold, setIsBold] = useState(false);
|
|
269
|
+
const [isItalic, setIsItalic] = useState(false);
|
|
270
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
271
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
272
|
+
const [codeLanguage, setCodeLanguage] = useState<string>("");
|
|
273
|
+
const [isEditable, setIsEditable] = useState(() => editor.isEditable());
|
|
274
|
+
|
|
275
|
+
const $updateToolbar = useCallback(() => {
|
|
276
|
+
const selection = $getSelection();
|
|
277
|
+
if ($isRangeSelection(selection)) {
|
|
278
|
+
const anchorNode = selection.anchor.getNode();
|
|
279
|
+
let element =
|
|
280
|
+
anchorNode.getKey() === "root"
|
|
281
|
+
? anchorNode
|
|
282
|
+
: $findMatchingParent(anchorNode, (e) => {
|
|
283
|
+
const parent = e.getParent();
|
|
284
|
+
return parent !== null && $isRootOrShadowRoot(parent);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (element === null) {
|
|
288
|
+
element = anchorNode.getTopLevelElementOrThrow();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const elementKey = element.getKey();
|
|
292
|
+
const elementDOM = activeEditor.getElementByKey(elementKey);
|
|
293
|
+
|
|
294
|
+
// Update text format
|
|
295
|
+
setIsCode(selection.hasFormat("code"));
|
|
296
|
+
setIsBold(selection.hasFormat("bold"));
|
|
297
|
+
setIsItalic(selection.hasFormat("italic"));
|
|
298
|
+
|
|
299
|
+
// Update links
|
|
300
|
+
const node = getSelectedNode(selection);
|
|
301
|
+
const parent = node.getParent();
|
|
302
|
+
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
303
|
+
setIsLink(true);
|
|
304
|
+
} else {
|
|
305
|
+
setIsLink(false);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (elementDOM !== null) {
|
|
309
|
+
setSelectedElementKey(elementKey);
|
|
310
|
+
if ($isListNode(element)) {
|
|
311
|
+
const parentList = $getNearestNodeOfType<ListNode>(
|
|
312
|
+
anchorNode,
|
|
313
|
+
ListNode,
|
|
314
|
+
);
|
|
315
|
+
const type = parentList
|
|
316
|
+
? parentList.getListType()
|
|
317
|
+
: element.getListType();
|
|
318
|
+
setBlockType(type);
|
|
319
|
+
} else {
|
|
320
|
+
const type = $isHeadingNode(element)
|
|
321
|
+
? element.getTag()
|
|
322
|
+
: element.getType();
|
|
323
|
+
if (type in blockTypeToBlockName) {
|
|
324
|
+
setBlockType(type as keyof typeof blockTypeToBlockName);
|
|
325
|
+
}
|
|
326
|
+
if ($isCodeNode(element)) {
|
|
327
|
+
const language =
|
|
328
|
+
element.getLanguage() as keyof typeof CODE_LANGUAGE_MAP;
|
|
329
|
+
setCodeLanguage(
|
|
330
|
+
language ? CODE_LANGUAGE_MAP[language] || language : '',
|
|
331
|
+
);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}, [activeEditor]);
|
|
338
|
+
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
return editor.registerCommand(
|
|
341
|
+
SELECTION_CHANGE_COMMAND,
|
|
342
|
+
(_payload, newEditor) => {
|
|
343
|
+
$updateToolbar();
|
|
344
|
+
setActiveEditor(newEditor);
|
|
345
|
+
return false;
|
|
346
|
+
},
|
|
347
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
348
|
+
);
|
|
349
|
+
}, [editor, $updateToolbar]);
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
return mergeRegister(
|
|
353
|
+
editor.registerEditableListener((editable) => {
|
|
354
|
+
setIsEditable(editable);
|
|
355
|
+
}),
|
|
356
|
+
activeEditor.registerUpdateListener(({ editorState }) => {
|
|
357
|
+
editorState.read(() => {
|
|
358
|
+
$updateToolbar();
|
|
359
|
+
});
|
|
360
|
+
}),
|
|
361
|
+
activeEditor.registerCommand<boolean>(
|
|
362
|
+
CAN_UNDO_COMMAND,
|
|
363
|
+
(payload) => {
|
|
364
|
+
setCanUndo(payload);
|
|
365
|
+
return false;
|
|
366
|
+
},
|
|
367
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
368
|
+
),
|
|
369
|
+
activeEditor.registerCommand<boolean>(
|
|
370
|
+
CAN_REDO_COMMAND,
|
|
371
|
+
(payload) => {
|
|
372
|
+
setCanRedo(payload);
|
|
373
|
+
return false;
|
|
374
|
+
},
|
|
375
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
}, [$updateToolbar, activeEditor, editor]);
|
|
379
|
+
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
return activeEditor.registerCommand(
|
|
382
|
+
KEY_MODIFIER_COMMAND,
|
|
383
|
+
(payload) => {
|
|
384
|
+
const event: KeyboardEvent = payload;
|
|
385
|
+
const { code, ctrlKey, metaKey } = event;
|
|
386
|
+
|
|
387
|
+
if (code === "KeyK" && (ctrlKey || metaKey)) {
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
if (!isLink) {
|
|
390
|
+
setIsLinkEditMode(true);
|
|
391
|
+
} else {
|
|
392
|
+
setIsLinkEditMode(false);
|
|
393
|
+
}
|
|
394
|
+
return activeEditor.dispatchCommand(
|
|
395
|
+
TOGGLE_LINK_COMMAND,
|
|
396
|
+
sanitizeUrl("https://"),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
400
|
+
},
|
|
401
|
+
COMMAND_PRIORITY_NORMAL,
|
|
402
|
+
);
|
|
403
|
+
}, [activeEditor, isLink, setIsLinkEditMode]);
|
|
404
|
+
|
|
405
|
+
const insertLink = useCallback(() => {
|
|
406
|
+
if (!isLink) {
|
|
407
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl("https://"));
|
|
408
|
+
} else {
|
|
409
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
410
|
+
}
|
|
411
|
+
}, [editor, isLink]);
|
|
412
|
+
|
|
413
|
+
const onCodeLanguageSelect = useCallback(
|
|
414
|
+
(value: string) => {
|
|
415
|
+
activeEditor.update(() => {
|
|
416
|
+
if (selectedElementKey !== null) {
|
|
417
|
+
const node = $getNodeByKey(selectedElementKey);
|
|
418
|
+
if ($isCodeNode(node)) {
|
|
419
|
+
node.setLanguage(value);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
[activeEditor, selectedElementKey],
|
|
425
|
+
);
|
|
426
|
+
return (
|
|
427
|
+
<div className="toolbar">
|
|
428
|
+
<button
|
|
429
|
+
disabled={!canUndo || !isEditable}
|
|
430
|
+
onClick={() => {
|
|
431
|
+
activeEditor.dispatchCommand(UNDO_COMMAND, undefined);
|
|
432
|
+
}}
|
|
433
|
+
title={IS_APPLE ? "Undo (⌘Z)" : "Undo (Ctrl+Z)"}
|
|
434
|
+
type="button"
|
|
435
|
+
className="toolbar-item spaced"
|
|
436
|
+
aria-label="Undo"
|
|
437
|
+
>
|
|
438
|
+
<i className="format undo" />
|
|
439
|
+
</button>
|
|
440
|
+
<button
|
|
441
|
+
disabled={!canRedo || !isEditable}
|
|
442
|
+
onClick={() => {
|
|
443
|
+
activeEditor.dispatchCommand(REDO_COMMAND, undefined);
|
|
444
|
+
}}
|
|
445
|
+
title={IS_APPLE ? "Redo (⌘Y)" : "Redo (Ctrl+Y)"}
|
|
446
|
+
type="button"
|
|
447
|
+
className="toolbar-item"
|
|
448
|
+
aria-label="Redo"
|
|
449
|
+
>
|
|
450
|
+
<i className="format redo" />
|
|
451
|
+
</button>
|
|
452
|
+
<Divider />
|
|
453
|
+
{blockType in blockTypeToBlockName && activeEditor === editor && (
|
|
454
|
+
<>
|
|
455
|
+
<BlockFormatDropDown
|
|
456
|
+
disabled={!isEditable}
|
|
457
|
+
blockType={blockType}
|
|
458
|
+
editor={editor}
|
|
459
|
+
/>
|
|
460
|
+
<Divider />
|
|
461
|
+
</>
|
|
462
|
+
)}
|
|
463
|
+
{blockType === "code" ? (
|
|
464
|
+
<DropDown
|
|
465
|
+
disabled={!isEditable}
|
|
466
|
+
buttonClassName="toolbar-item code-language"
|
|
467
|
+
buttonLabel={getLanguageFriendlyName(codeLanguage)}
|
|
468
|
+
buttonAriaLabel="Select language"
|
|
469
|
+
>
|
|
470
|
+
{CODE_LANGUAGE_OPTIONS.map(([value, name]) => {
|
|
471
|
+
return (
|
|
472
|
+
<DropDownItem
|
|
473
|
+
className={`item ${dropDownActiveClass(
|
|
474
|
+
value === codeLanguage,
|
|
475
|
+
)}`}
|
|
476
|
+
onClick={() => onCodeLanguageSelect(value)}
|
|
477
|
+
key={value}
|
|
478
|
+
>
|
|
479
|
+
<span className="text">{name}</span>
|
|
480
|
+
</DropDownItem>
|
|
481
|
+
);
|
|
482
|
+
})}
|
|
483
|
+
</DropDown>
|
|
484
|
+
) : (
|
|
485
|
+
<>
|
|
486
|
+
<button
|
|
487
|
+
disabled={!isEditable}
|
|
488
|
+
onClick={() => {
|
|
489
|
+
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
490
|
+
}}
|
|
491
|
+
className={"toolbar-item spaced " + (isBold ? "active" : "")}
|
|
492
|
+
title={IS_APPLE ? "Bold (⌘B)" : "Bold (Ctrl+B)"}
|
|
493
|
+
type="button"
|
|
494
|
+
aria-label={`Format text as bold. Shortcut: ${
|
|
495
|
+
IS_APPLE ? "⌘B" : "Ctrl+B"
|
|
496
|
+
}`}
|
|
497
|
+
>
|
|
498
|
+
<i className="format bold" />
|
|
499
|
+
</button>
|
|
500
|
+
<button
|
|
501
|
+
disabled={!isEditable}
|
|
502
|
+
onClick={() => {
|
|
503
|
+
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
|
|
504
|
+
}}
|
|
505
|
+
className={"toolbar-item spaced " + (isItalic ? "active" : "")}
|
|
506
|
+
title={IS_APPLE ? "Italic (⌘I)" : "Italic (Ctrl+I)"}
|
|
507
|
+
type="button"
|
|
508
|
+
aria-label={`Format text as italics. Shortcut: ${
|
|
509
|
+
IS_APPLE ? "⌘I" : "Ctrl+I"
|
|
510
|
+
}`}
|
|
511
|
+
>
|
|
512
|
+
<i className="format italic" />
|
|
513
|
+
</button>
|
|
514
|
+
<button
|
|
515
|
+
disabled={!isEditable}
|
|
516
|
+
onClick={insertLink}
|
|
517
|
+
className={"toolbar-item spaced " + (isLink ? "active" : "")}
|
|
518
|
+
aria-label="Insert link"
|
|
519
|
+
title="Insert link"
|
|
520
|
+
type="button"
|
|
521
|
+
>
|
|
522
|
+
<i className="format link" />
|
|
523
|
+
</button>
|
|
524
|
+
</>
|
|
525
|
+
)}
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
}
|