@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,47 @@
1
+ import * as React from "react";
2
+ import { Descendant, createEditor } from "slate";
3
+ import { withReact } from "slate-react";
4
+ import { BlogEditor, BlogEditorProps } from "./blog-rte";
5
+ import { PaperToolbar } from "./toolbar/paper-toolbar";
6
+ import withLinks from "./plugins/withLinks.js";
7
+ import withTables from "./plugins/withTable.js";
8
+ import withEmbeds from "./plugins/withEmbeds.js";
9
+ import withEquation from "./plugins/withEquation.js";
10
+ import { withHistory } from "slate-history";
11
+
12
+ export type PaperEditorProps = {
13
+ value?: Descendant[];
14
+ };
15
+
16
+ export function PaperEditor(props: PaperEditorProps) {
17
+ const [value, setValue] = React.useState(
18
+ props.value ||
19
+ ([
20
+ {
21
+ type: "p",
22
+ children: [{ text: "" }],
23
+ },
24
+ ] as Descendant[])
25
+ );
26
+ const editor = React.useMemo(
27
+ () =>
28
+ withEquation(
29
+ withHistory(
30
+ withEmbeds(withTables(withLinks(withReact(createEditor()))))
31
+ )
32
+ ),
33
+ []
34
+ );
35
+
36
+ return (
37
+ <>
38
+ <PaperToolbar editor={editor} value={value} />
39
+ <BlogEditor
40
+ readOnly={false}
41
+ value={value}
42
+ setValue={setValue}
43
+ editor={editor}
44
+ />
45
+ </>
46
+ );
47
+ }
@@ -0,0 +1,33 @@
1
+ import { Transforms, Path, Node } from "slate"
2
+
3
+ const withEmbeds = (editor) => {
4
+ const { isVoid, insertBreak } = editor
5
+
6
+ editor.isVoid = (element) =>
7
+ ["video", "image", "htmlCode"].includes(element.type) ? true : isVoid(element)
8
+
9
+ editor.insertBreak = (...args) => {
10
+ const parentPath = Path.parent(editor.selection.focus.path)
11
+ const parentNode = Node.get(editor, parentPath)
12
+ // console.log(parentNode);
13
+ if (editor.isVoid(parentNode)) {
14
+ const nextPath = Path.next(parentPath)
15
+ Transforms.insertNodes(
16
+ editor,
17
+ {
18
+ type: "p",
19
+ children: [{ text: "" }],
20
+ },
21
+ {
22
+ at: nextPath,
23
+ select: true, // Focus on this node once inserted
24
+ }
25
+ )
26
+ } else {
27
+ insertBreak(...args)
28
+ }
29
+ }
30
+ return editor
31
+ }
32
+
33
+ export default withEmbeds
@@ -0,0 +1,8 @@
1
+ const withEquation = (editor) =>{
2
+ const { isInline } = editor;
3
+ editor.isInline = (element) =>
4
+ element.type === 'equation' && element.inline ? true : isInline(element)
5
+ return editor;
6
+ }
7
+
8
+ export default withEquation;
@@ -0,0 +1,69 @@
1
+ import {
2
+ Descendant,
3
+ BaseEditor,
4
+ BaseRange,
5
+ Range,
6
+ Element,
7
+ Transforms,
8
+ BaseElement,
9
+ Editor,
10
+ } from "slate";
11
+ import { ImageElement } from "../components/image/types";
12
+ import { ReactEditor } from "slate-react";
13
+ import { HistoryEditor } from "slate-history";
14
+ // @ts-ignore
15
+ import imageExtensions from "image-extensions";
16
+ import isUrl from "is-url";
17
+
18
+ export const isImageUrl = (url: string) => {
19
+ if (!url) return false;
20
+ if (!isUrl(url)) return false;
21
+ return true;
22
+ // const ext = new URL(url).pathname.split(".").pop();
23
+ // return imageExtensions.includes(ext);
24
+ };
25
+
26
+ const insertImage = (editor: Editor, url: string) => {
27
+ const text = { text: "" };
28
+ const image: ImageElement = { type: "image", url, children: [text] };
29
+ Transforms.insertNodes(editor, image);
30
+ };
31
+
32
+ const withImages = (editor: Editor) => {
33
+ const { insertData, isVoid } = editor;
34
+
35
+ editor.isVoid = (element: BaseElement) => {
36
+ // @ts-ignore
37
+ return element.type === "image" ? true : isVoid(element);
38
+ };
39
+
40
+ editor.insertData = (data) => {
41
+ const text = data.getData("text/plain");
42
+ const { files } = data;
43
+
44
+ if (files && files.length > 0) {
45
+ // @ts-ignore
46
+ for (const file of files) {
47
+ const reader = new FileReader();
48
+ const [mime] = file.type.split("/");
49
+
50
+ if (mime === "image") {
51
+ reader.addEventListener("load", () => {
52
+ const url = reader.result;
53
+ insertImage(editor, url as string);
54
+ });
55
+
56
+ reader.readAsDataURL(file);
57
+ }
58
+ }
59
+ } else if (isImageUrl(text)) {
60
+ insertImage(editor, text);
61
+ } else {
62
+ insertData(data);
63
+ }
64
+ };
65
+
66
+ return editor;
67
+ };
68
+
69
+ export default withImages;
@@ -0,0 +1,9 @@
1
+ const withLinks = (editor)=>{
2
+
3
+ const { isInline } = editor;
4
+ editor.isInline = (element) =>
5
+ element.type === 'link' ? true :isInline(element);
6
+ return editor;
7
+ };
8
+
9
+ export default withLinks;
@@ -0,0 +1,74 @@
1
+ import { Editor, Range, Point, Element} from 'slate'
2
+
3
+ const withTable = (editor) =>{
4
+ const { deleteBackward, deleteForward, insertBreak} = editor
5
+
6
+ editor.deleteBackward = unit =>{
7
+ const { selection }= editor;
8
+ if (selection) {
9
+ const [cell] = Editor.nodes(editor, {
10
+ match: n =>
11
+ !Editor.isEditor(n) &&
12
+ Element.isElement(n) &&
13
+ n.type === 'table-cell',
14
+ })
15
+ const prevNodePath = Editor.before(editor,selection)
16
+
17
+ const [tableNode] = Editor.nodes(editor,{
18
+ at:prevNodePath,
19
+ match:n =>!Editor.isEditor(n) && Element.isElement && n.type === 'table-cell'
20
+ })
21
+
22
+ if (cell) {
23
+ const [, cellPath] = cell
24
+
25
+ const start = Editor.start(editor, cellPath)
26
+ if (Point.equals(selection.anchor, start)) {
27
+ return
28
+ }
29
+ }
30
+ if(!cell && tableNode){
31
+ return
32
+ }
33
+ }
34
+
35
+ deleteBackward(unit)
36
+ }
37
+ editor.deleteForward = unit => {
38
+ const { selection } = editor
39
+ if (selection && Range.isCollapsed(selection)) {
40
+ const [cell] = Editor.nodes(editor, {
41
+ match: n =>
42
+ !Editor.isEditor(n) &&
43
+ Element.isElement(n) &&
44
+ n.type === 'table-cell',
45
+ })
46
+
47
+ const prevNodePath = Editor.after(editor,selection)
48
+ const [tableNode] = Editor.nodes(editor,{
49
+ at:prevNodePath,
50
+ match:n =>!Editor.isEditor(n) && Element.isElement && n.type === 'table-cell'
51
+ })
52
+
53
+
54
+ if (cell) {
55
+ const [, cellPath] = cell
56
+ const end = Editor.end(editor, cellPath)
57
+
58
+ if (Point.equals(selection.anchor, end)) {
59
+ return
60
+ }
61
+ }
62
+ if(!cell && tableNode){
63
+ return
64
+ }
65
+ }
66
+
67
+ deleteForward(unit)
68
+ }
69
+
70
+ return editor;
71
+ }
72
+
73
+
74
+ export default withTable;
@@ -0,0 +1,44 @@
1
+ import escapeHTML from "escape-html";
2
+ import { TextNode, ParentNode } from "./types";
3
+
4
+ const formatValue = (k: string, v: string) => {
5
+ if (k === "href") {
6
+ return escapeHTML(v);
7
+ } else {
8
+ return v;
9
+ }
10
+ };
11
+
12
+ const attrToString = (attr: Record<string, string>) => {
13
+ return Object.entries(attr)
14
+ .map(([k, v]) => `${k}="${formatValue(k, v)}"`)
15
+ .join(" ");
16
+ };
17
+
18
+ // these assume that the markdown generated is gfm and supports html
19
+ const toBold = (text: string) => `<b>${text}</b>`;
20
+ const toItalic = (text: string) => `<i>${text}</i>`;
21
+ const toUnderline = (text: string) => `<u>${text}</u>`;
22
+ const toStrikethrough = (text: string) => `<del>${text}</del>`;
23
+ const toSubscript = (text: string) => `<sub>${text}</sub>`;
24
+ const toSuperscript = (text: string) => `<sup>${text}</sup>`;
25
+
26
+ export const serializeLink = (node: ParentNode, serializedChildren: string) => {
27
+ const { type, children, ...attr } = node;
28
+ if (type === "link") {
29
+ return `<a ${attrToString(attr)}>${serializedChildren}</a>`;
30
+ } else {
31
+ return "";
32
+ }
33
+ };
34
+
35
+ export const serializeText = (node: TextNode) => {
36
+ let string = node.text;
37
+ if (node.bold) string = toBold(string);
38
+ if (node.italic) string = toItalic(string);
39
+ if (node.underline) string = toUnderline(string);
40
+ if (node.strikethrough) string = toStrikethrough(string);
41
+ if (node.subscript) string = toSubscript(string);
42
+ if (node.superscript) string = toSuperscript(string);
43
+ return string;
44
+ };
@@ -0,0 +1,20 @@
1
+ export interface TextNode {
2
+ text: string
3
+ bold?: boolean
4
+ italic?: boolean
5
+ underline?: boolean
6
+ strikethrough?: boolean
7
+ subscript?: boolean
8
+ superscript?: boolean
9
+ }
10
+
11
+ export interface ParentNode {
12
+ type: string
13
+ children: TextNode[]
14
+ href?: string
15
+ target?: string
16
+ }
17
+
18
+ export function instanceOfTextNode(object: any): object is TextNode {
19
+ return "text" in object
20
+ }
@@ -0,0 +1,186 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { useSlate } from "slate-react";
3
+ import { Range } from "slate";
4
+ import Button from "../common/button";
5
+ import Icon from "../common/icon";
6
+ import {
7
+ Select,
8
+ Box,
9
+ MenuItem,
10
+ SelectChangeEvent,
11
+ FormControl,
12
+ } from "@mui/material";
13
+ import {
14
+ toggleBlock,
15
+ toggleMark,
16
+ isMarkActive,
17
+ addMarkData,
18
+ isBlockActive,
19
+ activeMark,
20
+ } from "../utils";
21
+ import useFormat from "../utils/customHooks/useFormat.js";
22
+ import LinkButton from "../components/link/LinkButton";
23
+ import Embed from "../components/embed/Embed";
24
+ import TableSelector from "../components/table/TableSelector";
25
+ import EquationButton from "../components/equation/EquationButton";
26
+ import Id from "../components/id/Id";
27
+ import TableContextMenu from "../components/table-context-menu/TableContextMenu";
28
+ import CodeToTextButton from "../components/code-to-text/CodeToTextButton";
29
+ import HtmlContextMenu from "../components/code-to-text/HtmlContextMenu";
30
+ import { ToolbarGroup } from "./toolbar-groups";
31
+ import { useShortcut } from "./shortcuts";
32
+ import { SlateColorButton } from "../components/color-picker/slate-color-button";
33
+ import { InsertImageButton } from "../components/image/insert-image-button";
34
+
35
+ export type ToolbarProps = {
36
+ handleCodeToText: Function;
37
+ toolbarGroups: Array<Array<ToolbarGroup>>;
38
+ };
39
+
40
+ export default function Toolbar(props: ToolbarProps) {
41
+ const { handleCodeToText } = props;
42
+ const editor = useSlate();
43
+ const isTable = useFormat(editor, "table");
44
+ const [toolbarGroups, setToolbarGroups] = useState(props.toolbarGroups);
45
+
46
+ useEffect(() => {
47
+ // Filter out the groups which are not allowed to be inserted when a table is in focus.
48
+ let filteredGroups = [...props.toolbarGroups];
49
+ if (isTable) {
50
+ filteredGroups = toolbarGroups.map((grp) =>
51
+ grp.filter(
52
+ (element) =>
53
+ //groups that are not supported inside the table
54
+ !["codeToText"].includes(element.type)
55
+ )
56
+ );
57
+ filteredGroups = filteredGroups.filter((elem) => elem.length);
58
+ }
59
+ setToolbarGroups(filteredGroups);
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, [isTable]);
62
+
63
+ const BlockButton = ({ format }: ToolbarGroup) => {
64
+ useShortcut(format, () => toggleBlock(editor, format));
65
+ return (
66
+ <Button
67
+ active={isBlockActive(editor, format)}
68
+ format={format}
69
+ onMouseDown={(e: MouseEvent) => {
70
+ e.preventDefault();
71
+ toggleBlock(editor, format);
72
+ }}
73
+ style={{ backgroundColor: "#f9fafc" }}
74
+ >
75
+ <Icon icon={format} />
76
+ </Button>
77
+ );
78
+ };
79
+
80
+ const MarkButton = ({ format }: ToolbarGroup) => {
81
+ useShortcut(format, () => toggleMark(editor, format));
82
+ return (
83
+ <Button
84
+ active={isMarkActive(editor, format)}
85
+ format={format}
86
+ onMouseDown={(e: MouseEvent) => {
87
+ e.preventDefault();
88
+ toggleMark(editor, format);
89
+ }}
90
+ style={{ backgroundColor: "#f9fafc" }}
91
+ >
92
+ <Icon icon={format} />
93
+ </Button>
94
+ );
95
+ };
96
+
97
+ const Dropdown = ({ format, options }: ToolbarGroup) => {
98
+ return (
99
+ <FormControl sx={{ m: 1, minWidth: 120 }} size="small">
100
+ <Select
101
+ value={activeMark(editor, format)}
102
+ onChange={(e) => changeMarkData(e, format)}
103
+ >
104
+ {options?.map((item, index) => (
105
+ <MenuItem key={index} value={item.value}>
106
+ {item.text}
107
+ </MenuItem>
108
+ ))}
109
+ </Select>
110
+ </FormControl>
111
+ );
112
+ };
113
+
114
+ const changeMarkData = (event: SelectChangeEvent<any>, format: string) => {
115
+ event.preventDefault();
116
+ const value = event.target.value;
117
+ console.log(value);
118
+ addMarkData(editor, { format, value });
119
+ };
120
+
121
+ return (
122
+ <Box
123
+ sx={{
124
+ display: "flex",
125
+ flexDirection: "row",
126
+ alignItems: "center",
127
+ flexWrap: "wrap",
128
+ }}
129
+ >
130
+ {toolbarGroups.map((group, index) => (
131
+ <Box key={index} sx={{ display: "flex" }}>
132
+ {group.map((element) => {
133
+ switch (element.type) {
134
+ case "block":
135
+ return <BlockButton key={element.id} {...element} />;
136
+ case "mark":
137
+ return <MarkButton key={element.id} {...element} />;
138
+ case "dropdown":
139
+ return <Dropdown key={element.id} {...element} />;
140
+ case "link":
141
+ return (
142
+ <LinkButton
143
+ key={element.id}
144
+ active={isBlockActive(editor, "link")}
145
+ editor={editor}
146
+ />
147
+ );
148
+ case "embed":
149
+ return (
150
+ <Embed
151
+ key={element.id}
152
+ format={element.format}
153
+ editor={editor}
154
+ />
155
+ );
156
+ case "color-picker":
157
+ return (
158
+ <SlateColorButton
159
+ key={element.id}
160
+ editor={editor}
161
+ format={element.format as "bgcolor" | "color"}
162
+ />
163
+ );
164
+ case "table":
165
+ return <TableSelector key={element.id} editor={editor} />;
166
+ case "id":
167
+ return <Id editor={editor} />;
168
+ case "equation":
169
+ return <EquationButton editor={editor} />;
170
+ case "codeToText":
171
+ return (
172
+ <CodeToTextButton handleButtonClick={handleCodeToText} />
173
+ );
174
+ case "image":
175
+ return <InsertImageButton editor={editor} />;
176
+ default:
177
+ return null;
178
+ }
179
+ })}
180
+ </Box>
181
+ ))}
182
+ <TableContextMenu editor={editor} />
183
+ <HtmlContextMenu editor={editor} handleCodeToText={handleCodeToText} />
184
+ </Box>
185
+ );
186
+ }