@springmicro/rte 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.eslintrc.cjs +18 -0
  2. package/README.md +15 -0
  3. package/dist/index.d.ts +10 -0
  4. package/dist/index.js +63921 -0
  5. package/dist/index.umd.cjs +469 -0
  6. package/dist/style.css +1 -0
  7. package/index.html +13 -0
  8. package/package.json +54 -0
  9. package/src/App.css +42 -0
  10. package/src/App.tsx +10 -0
  11. package/src/contexts/color-context.tsx +53 -0
  12. package/src/hooks/useSimpleFormik.tsx +74 -0
  13. package/src/index.css +68 -0
  14. package/src/index.tsx +3 -0
  15. package/src/main.tsx +10 -0
  16. package/src/slate/base-editor.stories.tsx +16 -0
  17. package/src/slate/base-editor.tsx +116 -0
  18. package/src/slate/blog-rte.stories.tsx +16 -0
  19. package/src/slate/blog-rte.tsx +126 -0
  20. package/src/slate/common/button.tsx +35 -0
  21. package/src/slate/common/element.tsx +13 -0
  22. package/src/slate/common/icon.jsx +97 -0
  23. package/src/slate/components/code-to-text/CodeToTextButton.jsx +19 -0
  24. package/src/slate/components/code-to-text/HtmlCode.jsx +64 -0
  25. package/src/slate/components/code-to-text/HtmlContextMenu.jsx +39 -0
  26. package/src/slate/components/code-to-text/index.jsx +111 -0
  27. package/src/slate/components/color-picker/color-cursor.stories.tsx +16 -0
  28. package/src/slate/components/color-picker/color-cursor.tsx +34 -0
  29. package/src/slate/components/color-picker/color-formats-view.stories.tsx +25 -0
  30. package/src/slate/components/color-picker/color-formats-view.tsx +115 -0
  31. package/src/slate/components/color-picker/color-gradient.stories.tsx +48 -0
  32. package/src/slate/components/color-picker/color-gradient.tsx +128 -0
  33. package/src/slate/components/color-picker/color-hue.stories.tsx +41 -0
  34. package/src/slate/components/color-picker/color-hue.tsx +110 -0
  35. package/src/slate/components/color-picker/color-picker.stories.tsx +25 -0
  36. package/src/slate/components/color-picker/color-picker.tsx +41 -0
  37. package/src/slate/components/color-picker/color-popover.stories.tsx +26 -0
  38. package/src/slate/components/color-picker/color-popover.tsx +58 -0
  39. package/src/slate/components/color-picker/color-swatch.stories.tsx +16 -0
  40. package/src/slate/components/color-picker/color-swatch.tsx +76 -0
  41. package/src/slate/components/color-picker/default-colors.ts +38 -0
  42. package/src/slate/components/color-picker/slate-color-button.tsx +128 -0
  43. package/src/slate/components/embed/Embed.jsx +96 -0
  44. package/src/slate/components/embed/Image.jsx +45 -0
  45. package/src/slate/components/embed/Video.jsx +65 -0
  46. package/src/slate/components/equation/Equation.jsx +19 -0
  47. package/src/slate/components/equation/EquationButton.jsx +68 -0
  48. package/src/slate/components/id/Id.jsx +57 -0
  49. package/src/slate/components/image/image.stories.tsx +17 -0
  50. package/src/slate/components/image/image.tsx +62 -0
  51. package/src/slate/components/image/insert-image-button.stories.tsx +83 -0
  52. package/src/slate/components/image/insert-image-button.tsx +132 -0
  53. package/src/slate/components/image/types.ts +9 -0
  54. package/src/slate/components/link/Link.jsx +56 -0
  55. package/src/slate/components/link/LinkButton.tsx +106 -0
  56. package/src/slate/components/table/Table.jsx +11 -0
  57. package/src/slate/components/table/TableSelector.jsx +97 -0
  58. package/src/slate/components/table-context-menu/TableContextMenu.tsx +106 -0
  59. package/src/slate/custom-types.d.ts +152 -0
  60. package/src/slate/editor.module.css +226 -0
  61. package/src/slate/paper-rte.stories.tsx +16 -0
  62. package/src/slate/paper-rte.tsx +47 -0
  63. package/src/slate/plugins/withEmbeds.js +33 -0
  64. package/src/slate/plugins/withEquation.js +8 -0
  65. package/src/slate/plugins/withImages.ts +69 -0
  66. package/src/slate/plugins/withLinks.js +9 -0
  67. package/src/slate/plugins/withTable.js +74 -0
  68. package/src/slate/serializers/generic.ts +44 -0
  69. package/src/slate/serializers/types.ts +20 -0
  70. package/src/slate/toolbar/index.tsx +186 -0
  71. package/src/slate/toolbar/paper-toolbar.tsx +494 -0
  72. package/src/slate/toolbar/shortcuts.tsx +77 -0
  73. package/src/slate/toolbar/toolbar-groups.ts +213 -0
  74. package/src/slate/types/index.ts +0 -0
  75. package/src/slate/utils/customHooks/useContextMenu.js +42 -0
  76. package/src/slate/utils/customHooks/useFormat.js +26 -0
  77. package/src/slate/utils/customHooks/usePopup.jsx +26 -0
  78. package/src/slate/utils/customHooks/useResize.js +27 -0
  79. package/src/slate/utils/embed.js +18 -0
  80. package/src/slate/utils/equation.js +22 -0
  81. package/src/slate/utils/index.jsx +267 -0
  82. package/src/slate/utils/link.js +44 -0
  83. package/src/slate/utils/p.js +4 -0
  84. package/src/slate/utils/table.js +131 -0
  85. package/src/vite-env.d.ts +1 -0
  86. package/tsconfig.json +32 -0
  87. package/tsconfig.node.json +10 -0
  88. package/vite.config.ts +41 -0
@@ -0,0 +1,494 @@
1
+ import {
2
+ MenuList,
3
+ MenuItem,
4
+ Button,
5
+ ButtonGroup,
6
+ Grow,
7
+ Popper,
8
+ ClickAwayListener,
9
+ Paper,
10
+ Box,
11
+ Typography,
12
+ } from "@mui/material";
13
+ import Icon from "../common/icon";
14
+ import * as React from "react";
15
+ import { BaseEditor, Descendant } from "slate";
16
+ import { slateToHtml, SlateToDomConfig } from "slate-serializers";
17
+ import { ParentNode, TextNode } from "../serializers/types";
18
+ import { Element, Node, ChildNode } from "domhandler";
19
+ import { fontFamilyMap, sizeMap } from "../utils";
20
+
21
+ import "html-to-rtf/app/browser/bundle.js";
22
+
23
+ // @ts-ignore
24
+ // dynamic(() => import("html-to-rtf/app/browser/bundle.js"), { ssr: false });
25
+
26
+ export type PaperToolbarProps = {
27
+ value: Descendant[];
28
+ editor: BaseEditor;
29
+ };
30
+
31
+ function download(filename: string, text: string, mime: string) {
32
+ let element = document.createElement("a");
33
+ element.setAttribute(
34
+ "href",
35
+ `data:${mime};charset=utf-8,` + encodeURIComponent(text)
36
+ );
37
+ element.setAttribute("download", filename);
38
+
39
+ element.style.display = "none";
40
+ document.body.appendChild(element);
41
+
42
+ element.click();
43
+
44
+ document.body.removeChild(element);
45
+ }
46
+
47
+ const PAGE_MARGIN_X_INCHES = 0.5;
48
+ const PAGE_MARGIN_Y_INCHES = 1;
49
+ const EXPORT_STYLES =
50
+ "font-family: Helvetica, Arial, sans serif; font-size: 16px;"; // When exporting the margins will be figured out by the PDF renderer, so we don't need to add them here
51
+ const DEFAULT_STYLES =
52
+ EXPORT_STYLES +
53
+ `padding: ${PAGE_MARGIN_X_INCHES}in ${PAGE_MARGIN_Y_INCHES}in; `; // I still want the padding when regularly exporting to HTML
54
+
55
+ // Typescript HATES this code, but it works B)
56
+ // Feel free to refactor it if you want, but I'm pretty sure the problem is that we are using the same "type" but from different libraries
57
+ // so typescript thinks they are different types even though they are the same (at keast for some of them)
58
+ // Default config + lists and colors and tweaks to links and blockquotes
59
+ export const slate_config: SlateToDomConfig = {
60
+ elementMap: {
61
+ p: "p",
62
+ paragraph: "p",
63
+ h1: "h1",
64
+ h2: "h2",
65
+ h3: "h3",
66
+ h4: "h4",
67
+ h5: "h5",
68
+ h6: "h6",
69
+ ["table-row"]: "tr",
70
+ ["table-cell"]: "td",
71
+ },
72
+ markMap: {
73
+ strikethrough: ["s"],
74
+ bold: ["strong"],
75
+ underline: ["u"],
76
+ italic: ["i"],
77
+ code: ["pre", "code"],
78
+ },
79
+ elementTransforms: {
80
+ // Need indentation in PDF export
81
+ //@ts-ignore
82
+ blockquote: ({ node, children = [] }) => {
83
+ //@ts-ignore
84
+ const p = [new Element("p", {}, children)];
85
+ return new Element("blockquote", {}, p);
86
+ },
87
+ //@ts-ignore
88
+ link: ({ node, children = [] }) => {
89
+ const attributes = {
90
+ target: "",
91
+ };
92
+ attributes.target = node.target;
93
+ return new Element(
94
+ "a",
95
+ {
96
+ href: node.href,
97
+ ...attributes,
98
+ },
99
+ children as unknown as ChildNode[] // keep this change in merge
100
+ );
101
+ },
102
+ //@ts-ignore
103
+ unorderedList: ({ node, children = [] }) => {
104
+ const new_children = children.map(
105
+ //@ts-ignore
106
+ (child) => new Element("li", {}, [child])
107
+ );
108
+ return new Element("ul", {}, new_children);
109
+ },
110
+ //@ts-ignore
111
+ orderedList: ({ children = [] }) => {
112
+ const new_children = children.map(
113
+ //@ts-ignore
114
+ (child) => new Element("li", {}, [child])
115
+ );
116
+ return new Element("ol", {}, new_children);
117
+ },
118
+ //@ts-ignore
119
+ image: ({ node }) => {
120
+ const attributes = {
121
+ src: "",
122
+ };
123
+ attributes.src = node.url;
124
+ return new Element("img", attributes, []);
125
+ },
126
+ //@ts-ignore
127
+ alignCenter: ({ node, children = [] }) => {
128
+ const attributes = {
129
+ style: "",
130
+ };
131
+ attributes.style = `text-align: center; display: block;`;
132
+ //@ts-ignore
133
+ return new Element("div", attributes, children);
134
+ },
135
+ //@ts-ignore
136
+ alignRight: ({ node, children = [] }) => {
137
+ const attributes = {
138
+ style: "",
139
+ };
140
+ attributes.style = `text-align: right; display: block;`;
141
+ //@ts-ignore
142
+ return new Element("div", attributes, children);
143
+ },
144
+ //@ts-ignore
145
+ alignLeft: ({ node, children = [] }) => {
146
+ const attributes = {
147
+ style: "",
148
+ };
149
+ attributes.style = `text-align: left; display: block;`;
150
+ //@ts-ignore
151
+ return new Element("div", attributes, children);
152
+ },
153
+ //@ts-ignore
154
+ table: ({ node, children = [] }) => {
155
+ const attributes = {
156
+ border: "",
157
+ };
158
+ attributes.border = `1`;
159
+ //@ts-ignore
160
+ return new Element("table", attributes, children);
161
+ },
162
+ },
163
+ markTransforms: {
164
+ //@ts-ignore
165
+ color: ({ node }) => {
166
+ const attributes = {
167
+ style: "",
168
+ };
169
+ attributes.style = `color: ${node.color}`;
170
+ return new Element("span", attributes, node.children);
171
+ },
172
+ //@ts-ignore
173
+ bgColor: ({ node }) => {
174
+ const attributes = {
175
+ style: "",
176
+ };
177
+ attributes.style = `background-color: ${node.bgColor}`;
178
+ return new Element("span", attributes, node.children);
179
+ },
180
+ //@ts-ignore
181
+ fontFamily: ({ node }) => {
182
+ const attributes = {
183
+ style: "",
184
+ };
185
+
186
+ // I don't know if using this random import is better or worse than just using an if statement for each font
187
+ // on one hand it means one less place to update duplicate data (DRY)
188
+ // on the other hand it's a random import to a random index.jsx file that I don't know the purpose of and having dependencies to random unrelated files is bad
189
+ let typedMap = fontFamilyMap as Record<string, string>;
190
+
191
+ attributes.style = `font-family: ${typedMap[node.fontFamily]}`;
192
+
193
+ return new Element("span", attributes, node.children);
194
+ },
195
+ //@ts-ignore
196
+ fontSize: ({ node }) => {
197
+ const attributes = {
198
+ style: "",
199
+ };
200
+
201
+ // This is the same as above with the fontFamilyMap
202
+ let typedMap = sizeMap as Record<string, string>;
203
+ attributes.style = `font-size: ${typedMap[node.fontSize]}`;
204
+ return new Element("span", attributes, node.children);
205
+ },
206
+ //@ts-ignore
207
+ superscript: ({ node }) => {
208
+ const attributes = {
209
+ style: "",
210
+ };
211
+ attributes.style = `vertical-align: super; font-size: smaller;`;
212
+ return new Element("span", attributes, node.children);
213
+ },
214
+ //@ts-ignore
215
+ subscript: ({ node }) => {
216
+ const attributes = {
217
+ style: "",
218
+ };
219
+ attributes.style = `vertical-align: sub; font-size: smaller;`;
220
+ return new Element("span", attributes, node.children);
221
+ },
222
+ },
223
+ };
224
+
225
+ // Made this wrapper function to make it easier to change the default styles (DRY code!)
226
+ const parseToHTML = (value: Descendant[], styles?: string) => {
227
+ if (styles == undefined) {
228
+ styles = DEFAULT_STYLES;
229
+ }
230
+ return (
231
+ `<div style='${styles}'>` + slateToHtml(value, slate_config) + "</div>"
232
+ );
233
+ };
234
+
235
+ // 🐢 TODO: Figure out how to add submenus, and then add them for the "Export as" option
236
+ const menuOptions = {
237
+ file: {
238
+ "Log html to console and clipboard": (value: Descendant[]) => {
239
+ const html = parseToHTML(value);
240
+ console.log(html);
241
+ navigator.clipboard.writeText(html);
242
+ },
243
+ Divider: "line",
244
+ "Download as .html file": (value: Descendant[]) => {
245
+ const html = parseToHTML(value);
246
+ download("markup.html", html, "text/html");
247
+ },
248
+ // "Download as .md file": (editor: TextNode | ParentNode) => {
249
+ // const markdown = serializeMD(editor)
250
+ // download("markdown.md", markdown, "text/plain")
251
+ // console.log(markdown)
252
+ // },
253
+ "Download as .rtf file": (value: Descendant[]) => {
254
+ // @ts-ignore
255
+ const rtf = window.htmlToRtf(parseToHTML(value));
256
+ download("rich.rtf", rtf, "text/rtf");
257
+ console.log(rtf);
258
+ },
259
+ // Untested because I can't test in Storybook due to CORS, and I do not know where in the app to go to test this 😎
260
+ "Download as .pdf file": (value: Descendant[]) => {
261
+ const htmlContent = parseToHTML(value, EXPORT_STYLES);
262
+
263
+ const response = fetch("http://localhost:3000/api/editor/export-pdf", {
264
+ method: "POST",
265
+ headers: {
266
+ "Content-Type": "application/json",
267
+ },
268
+ body: JSON.stringify({ htmlContent }),
269
+ })
270
+ .then((response) => response.blob())
271
+ .then((blob) => {
272
+ const blobUrl = URL.createObjectURL(blob);
273
+ download("document.pdf", blobUrl, "application/pdf");
274
+
275
+ // Make sure to release the URL object when you're done with it
276
+ URL.revokeObjectURL(blobUrl);
277
+ });
278
+ },
279
+ } as Record<string, Function | string>,
280
+ edit: {
281
+ Undo: () => {},
282
+ Redo: () => {},
283
+ "The names of entries that return line dont matter": "line",
284
+ Cut: () => {},
285
+ Copy: () => {},
286
+ Paste: () => {},
287
+ "These entries are the dividing lines between sections": "line",
288
+ "Select All": () => {},
289
+ Delete: () => {},
290
+ "Maybe this is a bad system, but it's simple to edit on the fly. Once groups are more settled we can hard code them":
291
+ "line",
292
+ "Find and Replace": () => {},
293
+ } as Record<string, Function | string>,
294
+ view: {
295
+ "Toggle Fullscreen": () => {},
296
+ "Toggle Paper View": () => {},
297
+ } as Record<string, Function | string>,
298
+ format: {
299
+ // Having menu options for all of these things we already have buttons for feels redundant, but that's how google docs does it. We can change it later if we want, especially since there's no code for these yet
300
+ "Paper Margins": () => {},
301
+ "Paper Dimensions": () => {},
302
+ Divider1: "line",
303
+ Bold: () => {},
304
+ Italic: () => {},
305
+ Underline: () => {},
306
+ Strikethrough: () => {},
307
+ Divider2: "line",
308
+ Superscript: () => {},
309
+ Subscript: () => {},
310
+ Divider3: "line",
311
+ "Font Family": () => {},
312
+ "Font Size": () => {},
313
+ Divider4: "line",
314
+ "Text Color": () => {},
315
+ "Background Color": () => {},
316
+ Divider5: "line",
317
+ "Align Left": () => {},
318
+ "Align Center": () => {},
319
+ "Align Right": () => {},
320
+ } as Record<string, Function | string>,
321
+ insert: {
322
+ Link: () => {},
323
+ Table: () => {},
324
+ Equation: () => {},
325
+ Image: () => {},
326
+ Code: () => {},
327
+ } as Record<string, Function | string>,
328
+ help: {
329
+ "Keyboard Shortcuts": () => {},
330
+ Divider: "line",
331
+ About: () => {},
332
+ Divider1: "line",
333
+ "Ask Keith": () => {},
334
+ } as Record<string, Function | string>,
335
+ } as Record<string, Record<string, Function | string>>;
336
+
337
+ //These are the options that you shouldn't be able to access if you haven't selected anything
338
+ const selectDependentOptions = ["Cut", "Copy", "Delete"];
339
+
340
+ export function PaperToolbar({ editor, value }: PaperToolbarProps) {
341
+ const menuOpenState = {
342
+ file: false,
343
+ edit: false,
344
+ view: false,
345
+ format: false,
346
+ insert: false,
347
+ help: false,
348
+ } as Record<string, boolean>;
349
+
350
+ const [open, setOpen] = React.useState(menuOpenState);
351
+ const anchorRef = React.useRef<HTMLDivElement>(null);
352
+ const [selectedIndex, setSelectedIndex] = React.useState(1);
353
+
354
+ // const handleClick = () => {
355
+ // console.info(`You clicked ${Object.keys(fileOptions)[selectedIndex]}`)
356
+ // fileOptions[Object.keys(fileOptions)[selectedIndex]](editor)
357
+ // }
358
+
359
+ const handleMenuItemClick = (
360
+ event: React.MouseEvent<HTMLLIElement, MouseEvent>,
361
+ menuOptions: Record<string, Function>,
362
+ index: number
363
+ ) => {
364
+ // setSelectedIndex(index)
365
+ const clickedOption = Object.keys(menuOptions)[index];
366
+ console.info(`You clicked ${clickedOption}`);
367
+ console.log(value);
368
+ menuOptions[clickedOption](value);
369
+ setOpen((prevOpen) => ({ ...prevOpen, file: false }));
370
+ };
371
+
372
+ const handleToggle = (menuItem: string) => {
373
+ console.log("You clicked " + menuItem);
374
+ console.log(
375
+ editor.selection?.anchor.offset == editor.selection?.focus.offset
376
+ );
377
+ console.log(editor.selection?.anchor.offset);
378
+ console.log(editor.selection?.focus.offset);
379
+ setOpen((prevOpen) => ({
380
+ ...prevOpen,
381
+ [menuItem]: !prevOpen[menuItem] as boolean,
382
+ }));
383
+ };
384
+
385
+ const handleClose = (event: Event, menuItem: string) => {
386
+ if (
387
+ anchorRef.current &&
388
+ anchorRef.current
389
+ .querySelector(`.${menuItem}`)
390
+ ?.contains(event.target as HTMLElement)
391
+ ) {
392
+ return;
393
+ }
394
+
395
+ console.log("You closed " + menuItem);
396
+ setOpen((prevOpen) => ({ ...prevOpen, [menuItem]: false }));
397
+ };
398
+
399
+ return (
400
+ <>
401
+ <ButtonGroup variant="outlined" ref={anchorRef} aria-label="split button">
402
+ {Object.keys(menuOpenState).map((key) => {
403
+ return (
404
+ <Button
405
+ size="small"
406
+ aria-controls={open[key] ? "split-button-menu" : undefined}
407
+ aria-expanded={open[key] ? "true" : undefined}
408
+ aria-haspopup="menu"
409
+ onClick={() => handleToggle(key)}
410
+ sx={{ pr: 1 }}
411
+ className={key}
412
+ key={key}
413
+ >
414
+ {key.charAt(0).toUpperCase() + key.slice(1)}
415
+ <Icon icon="arrowDown" />
416
+ </Button>
417
+ );
418
+ })}
419
+ </ButtonGroup>
420
+ {Object.keys(menuOpenState).map((key) => {
421
+ return (
422
+ <Popper
423
+ sx={{
424
+ zIndex: 1,
425
+ left: 0,
426
+ }}
427
+ open={open[key]}
428
+ anchorEl={anchorRef.current?.querySelector(`.${key}`)}
429
+ role={undefined} //What does this do?
430
+ transition
431
+ disablePortal
432
+ placement="bottom-start"
433
+ key={key}
434
+ >
435
+ {({ TransitionProps, placement }) => (
436
+ <Grow
437
+ {...TransitionProps}
438
+ style={{
439
+ transformOrigin: "center bottom",
440
+ }}
441
+ >
442
+ <Paper>
443
+ <ClickAwayListener
444
+ onClickAway={(event) => handleClose(event, key)}
445
+ >
446
+ <MenuList id="split-button-menu" autoFocusItem>
447
+ {Object.keys(menuOptions[key]).map((option, index) => {
448
+ if (menuOptions[key][option] == "line") {
449
+ // 🐢 -(The hard coded color here should probably come from a theme or something like that?)
450
+ return (
451
+ <Box
452
+ key={option}
453
+ sx={{
454
+ mx: 0,
455
+ my: 0.5,
456
+ borderBottom: "solid 2px #f2f2f2",
457
+ }}
458
+ />
459
+ );
460
+ }
461
+ return (
462
+ <MenuItem
463
+ key={option}
464
+ disabled={
465
+ selectDependentOptions.includes(option) &&
466
+ editor.selection?.anchor.offset ==
467
+ editor.selection?.focus.offset
468
+ ? true
469
+ : false
470
+ }
471
+ // selected={index === selectedIndex}
472
+ onClick={(event) =>
473
+ handleMenuItemClick(
474
+ event,
475
+ menuOptions[key] as Record<string, Function>,
476
+ index
477
+ )
478
+ }
479
+ >
480
+ {option}
481
+ </MenuItem>
482
+ );
483
+ })}
484
+ </MenuList>
485
+ </ClickAwayListener>
486
+ </Paper>
487
+ </Grow>
488
+ )}
489
+ </Popper>
490
+ );
491
+ })}
492
+ </>
493
+ );
494
+ }
@@ -0,0 +1,77 @@
1
+ import * as React from "react";
2
+
3
+ const CTRL = "Ctrl";
4
+ const CTRL_SHIFT = "Ctrl+Shift";
5
+ const ALT_SHIFT = "Alt+Shift";
6
+
7
+ const CTRL_USED_BY_EDITOR = [
8
+ "b",
9
+ "i",
10
+ "u",
11
+ ".",
12
+ ",",
13
+ "1",
14
+ "2",
15
+ "3",
16
+ "k",
17
+ "7",
18
+ "8",
19
+ ];
20
+
21
+ /**
22
+ * Modelled directly after Google Docs' shortcut keys.
23
+ * Custom shortcuts unique to us: Headings.
24
+ * @returns [key, combo]
25
+ */
26
+ export function getShortcutKey(format: string, toPrint?: boolean) {
27
+ let combo = CTRL;
28
+ if (format === "bold") return ["b", combo];
29
+ if (format === "italic") return ["i", combo];
30
+ if (format === "underline") return ["u", combo];
31
+ if (format === "superscript") return [".", combo];
32
+ if (format === "subscript") return [",", combo];
33
+ if (format === "h1") return ["1", combo];
34
+ if (format === "h2") return ["2", combo];
35
+ if (format === "h3") return ["3", combo];
36
+ if (format === "link") return ["k", combo];
37
+ combo = CTRL_SHIFT;
38
+ if (format === "orderedList") return toPrint ? ["7", combo] : ["*", combo];
39
+ if (format === "unorderedList") return toPrint ? ["8", combo] : ["&", combo];
40
+ if (format === "blockquote") return toPrint ? [".", combo] : [">", combo];
41
+ combo = ALT_SHIFT;
42
+ if (format === "strikethrough") return toPrint ? ["5", combo] : ["%", combo];
43
+ return ["", ""];
44
+ }
45
+
46
+ export function useShortcut(format: string, action: Function) {
47
+ React.useEffect(() => {
48
+ const [key, combo] = getShortcutKey(format);
49
+ if (key === "") return;
50
+ if ((window as any).keyMappings === undefined) {
51
+ console.log("create keymappings");
52
+ (window as any).keyMappings = {
53
+ [CTRL]: {} as Record<string, Function>,
54
+ [ALT_SHIFT]: {} as Record<string, Function>,
55
+ [CTRL_SHIFT]: {} as Record<string, Function>,
56
+ };
57
+ window.onkeydown = (e) => {
58
+ if (
59
+ (e.ctrlKey && CTRL_USED_BY_EDITOR.includes(e.key)) ||
60
+ (e.altKey && e.shiftKey)
61
+ ) {
62
+ e.preventDefault();
63
+ const combo = e.ctrlKey
64
+ ? e.shiftKey
65
+ ? CTRL_SHIFT
66
+ : CTRL
67
+ : ALT_SHIFT;
68
+ const action = (window as any).keyMappings[combo][e.key];
69
+ if (action) action();
70
+ }
71
+ };
72
+ }
73
+ if (!(window as any).keyMappings[combo][key]) {
74
+ (window as any).keyMappings[combo][key] = action;
75
+ }
76
+ }, [window]);
77
+ }