@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.
- package/.eslintrc.cjs +18 -0
- package/README.md +15 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +63921 -0
- package/dist/index.umd.cjs +469 -0
- package/dist/style.css +1 -0
- package/index.html +13 -0
- package/package.json +54 -0
- package/src/App.css +42 -0
- package/src/App.tsx +10 -0
- package/src/contexts/color-context.tsx +53 -0
- package/src/hooks/useSimpleFormik.tsx +74 -0
- package/src/index.css +68 -0
- package/src/index.tsx +3 -0
- package/src/main.tsx +10 -0
- package/src/slate/base-editor.stories.tsx +16 -0
- package/src/slate/base-editor.tsx +116 -0
- package/src/slate/blog-rte.stories.tsx +16 -0
- package/src/slate/blog-rte.tsx +126 -0
- package/src/slate/common/button.tsx +35 -0
- package/src/slate/common/element.tsx +13 -0
- package/src/slate/common/icon.jsx +97 -0
- package/src/slate/components/code-to-text/CodeToTextButton.jsx +19 -0
- package/src/slate/components/code-to-text/HtmlCode.jsx +64 -0
- package/src/slate/components/code-to-text/HtmlContextMenu.jsx +39 -0
- package/src/slate/components/code-to-text/index.jsx +111 -0
- package/src/slate/components/color-picker/color-cursor.stories.tsx +16 -0
- package/src/slate/components/color-picker/color-cursor.tsx +34 -0
- package/src/slate/components/color-picker/color-formats-view.stories.tsx +25 -0
- package/src/slate/components/color-picker/color-formats-view.tsx +115 -0
- package/src/slate/components/color-picker/color-gradient.stories.tsx +48 -0
- package/src/slate/components/color-picker/color-gradient.tsx +128 -0
- package/src/slate/components/color-picker/color-hue.stories.tsx +41 -0
- package/src/slate/components/color-picker/color-hue.tsx +110 -0
- package/src/slate/components/color-picker/color-picker.stories.tsx +25 -0
- package/src/slate/components/color-picker/color-picker.tsx +41 -0
- package/src/slate/components/color-picker/color-popover.stories.tsx +26 -0
- package/src/slate/components/color-picker/color-popover.tsx +58 -0
- package/src/slate/components/color-picker/color-swatch.stories.tsx +16 -0
- package/src/slate/components/color-picker/color-swatch.tsx +76 -0
- package/src/slate/components/color-picker/default-colors.ts +38 -0
- package/src/slate/components/color-picker/slate-color-button.tsx +128 -0
- package/src/slate/components/embed/Embed.jsx +96 -0
- package/src/slate/components/embed/Image.jsx +45 -0
- package/src/slate/components/embed/Video.jsx +65 -0
- package/src/slate/components/equation/Equation.jsx +19 -0
- package/src/slate/components/equation/EquationButton.jsx +68 -0
- package/src/slate/components/id/Id.jsx +57 -0
- package/src/slate/components/image/image.stories.tsx +17 -0
- package/src/slate/components/image/image.tsx +62 -0
- package/src/slate/components/image/insert-image-button.stories.tsx +83 -0
- package/src/slate/components/image/insert-image-button.tsx +132 -0
- package/src/slate/components/image/types.ts +9 -0
- package/src/slate/components/link/Link.jsx +56 -0
- package/src/slate/components/link/LinkButton.tsx +106 -0
- package/src/slate/components/table/Table.jsx +11 -0
- package/src/slate/components/table/TableSelector.jsx +97 -0
- package/src/slate/components/table-context-menu/TableContextMenu.tsx +106 -0
- package/src/slate/custom-types.d.ts +152 -0
- package/src/slate/editor.module.css +226 -0
- package/src/slate/paper-rte.stories.tsx +16 -0
- package/src/slate/paper-rte.tsx +47 -0
- package/src/slate/plugins/withEmbeds.js +33 -0
- package/src/slate/plugins/withEquation.js +8 -0
- package/src/slate/plugins/withImages.ts +69 -0
- package/src/slate/plugins/withLinks.js +9 -0
- package/src/slate/plugins/withTable.js +74 -0
- package/src/slate/serializers/generic.ts +44 -0
- package/src/slate/serializers/types.ts +20 -0
- package/src/slate/toolbar/index.tsx +186 -0
- package/src/slate/toolbar/paper-toolbar.tsx +494 -0
- package/src/slate/toolbar/shortcuts.tsx +77 -0
- package/src/slate/toolbar/toolbar-groups.ts +213 -0
- package/src/slate/types/index.ts +0 -0
- package/src/slate/utils/customHooks/useContextMenu.js +42 -0
- package/src/slate/utils/customHooks/useFormat.js +26 -0
- package/src/slate/utils/customHooks/usePopup.jsx +26 -0
- package/src/slate/utils/customHooks/useResize.js +27 -0
- package/src/slate/utils/embed.js +18 -0
- package/src/slate/utils/equation.js +22 -0
- package/src/slate/utils/index.jsx +267 -0
- package/src/slate/utils/link.js +44 -0
- package/src/slate/utils/p.js +4 -0
- package/src/slate/utils/table.js +131 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +32 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +41 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StoryFn, Meta } from "@storybook/react";
|
|
3
|
+
import { ColorHue, ColorHueProps } from "./color-hue";
|
|
4
|
+
import { colorStringToColorFormats } from "@springmicro/utils/editor";
|
|
5
|
+
|
|
6
|
+
type ColorHueWrapperProps = {
|
|
7
|
+
initialColor: string;
|
|
8
|
+
width: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function ColorHueWrapper({ initialColor, width }: ColorHueWrapperProps) {
|
|
12
|
+
const [colorFormats, setColorFormats] = React.useState(
|
|
13
|
+
colorStringToColorFormats(initialColor)
|
|
14
|
+
);
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
setColorFormats(colorStringToColorFormats(initialColor));
|
|
17
|
+
}, [initialColor]);
|
|
18
|
+
return (
|
|
19
|
+
<ColorHue
|
|
20
|
+
width={width}
|
|
21
|
+
colorFormats={colorFormats}
|
|
22
|
+
setColorFormats={setColorFormats}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
/* 👇 The title prop is optional.
|
|
29
|
+
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
|
30
|
+
* to learn how to generate automatic titles
|
|
31
|
+
*/
|
|
32
|
+
title: "Slate Color Picker/Color Hue",
|
|
33
|
+
component: ColorHueWrapper,
|
|
34
|
+
} as Meta<typeof ColorHueWrapper>;
|
|
35
|
+
|
|
36
|
+
export const Primary = {
|
|
37
|
+
args: {
|
|
38
|
+
initialColor: "#00ff00",
|
|
39
|
+
width: 360,
|
|
40
|
+
} as ColorHueWrapperProps,
|
|
41
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useState, useRef, Dispatch, SetStateAction, useEffect } from "react";
|
|
2
|
+
import { Box } from "@mui/material";
|
|
3
|
+
import {
|
|
4
|
+
hueFromColorFormats,
|
|
5
|
+
newColorFormatsFromHue,
|
|
6
|
+
} from "@springmicro/utils/editor";
|
|
7
|
+
import { ColorFormats } from "@springmicro/utils/types";
|
|
8
|
+
import { ColorCursor } from "./color-cursor";
|
|
9
|
+
|
|
10
|
+
const hueMax = 360;
|
|
11
|
+
|
|
12
|
+
const calcPosFromHue = (hue: number, width: number) => {
|
|
13
|
+
return (width * hue) / hueMax;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const calcHueFromPos = (x: number, w: number) => {
|
|
17
|
+
return Math.round(Math.min(hueMax, Math.max(0, (hueMax * x) / w)));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ColorHueProps = {
|
|
21
|
+
width: number;
|
|
22
|
+
colorFormats: ColorFormats;
|
|
23
|
+
setColorFormats: Dispatch<SetStateAction<ColorFormats>>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function ColorHue({
|
|
27
|
+
width,
|
|
28
|
+
colorFormats,
|
|
29
|
+
setColorFormats,
|
|
30
|
+
}: ColorHueProps) {
|
|
31
|
+
const height = 15;
|
|
32
|
+
const ref = useRef();
|
|
33
|
+
const [mouseIsDown, setMouseIsDown] = useState(false);
|
|
34
|
+
const [cursorPosition, setCursorPosition] = useState({
|
|
35
|
+
x: calcPosFromHue(hueFromColorFormats(colorFormats), width),
|
|
36
|
+
y: (height * 2) / 3,
|
|
37
|
+
});
|
|
38
|
+
const [rect, setRect] = useState<DOMRect>();
|
|
39
|
+
|
|
40
|
+
const updateHue = (x: number, y: number, w: number, h: number) => {
|
|
41
|
+
const newHue = calcHueFromPos(x, w);
|
|
42
|
+
setColorFormats(newColorFormatsFromHue(colorFormats, newHue));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setCursorPosition({
|
|
47
|
+
x: calcPosFromHue(hueFromColorFormats(colorFormats), width),
|
|
48
|
+
y: cursorPosition.y,
|
|
49
|
+
});
|
|
50
|
+
}, [colorFormats]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Box
|
|
54
|
+
ref={ref}
|
|
55
|
+
onMouseDown={(e) => {
|
|
56
|
+
if (!ref.current) return;
|
|
57
|
+
setMouseIsDown(true);
|
|
58
|
+
const rect = (ref.current as HTMLElement).getBoundingClientRect();
|
|
59
|
+
setRect(rect);
|
|
60
|
+
// e.clientX was not giving the right position within the element
|
|
61
|
+
// calculate instead
|
|
62
|
+
const x = e.pageX - rect.x;
|
|
63
|
+
const y = e.pageY - rect.y;
|
|
64
|
+
updateHue(x, y, rect.width, rect.height);
|
|
65
|
+
}}
|
|
66
|
+
onMouseUp={(e) => {
|
|
67
|
+
setMouseIsDown(false);
|
|
68
|
+
}}
|
|
69
|
+
onMouseMove={(e) => {
|
|
70
|
+
if (mouseIsDown && rect) {
|
|
71
|
+
const x = e.pageX - rect.x;
|
|
72
|
+
const y = e.pageY - rect.y;
|
|
73
|
+
if (x <= width && y <= height && x >= 0 && y >= 0) {
|
|
74
|
+
// console.log(x, y, rect.x, rect.y)
|
|
75
|
+
updateHue(x, y, rect.width, rect.height);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
sx={{
|
|
80
|
+
position: "relative",
|
|
81
|
+
height,
|
|
82
|
+
width,
|
|
83
|
+
background: `linear-gradient(
|
|
84
|
+
to right,
|
|
85
|
+
rgba(255, 0, 0, 1) 0%,
|
|
86
|
+
rgba(255, 255, 0, 1) 17%,
|
|
87
|
+
rgba(0, 255, 0, 1) 34%,
|
|
88
|
+
rgba(0, 255, 255, 1) 51%,
|
|
89
|
+
rgba(0, 0, 255, 1) 68%,
|
|
90
|
+
rgba(255, 0, 255, 1) 85%,
|
|
91
|
+
rgba(255, 0, 0, 1) 100%
|
|
92
|
+
)`,
|
|
93
|
+
borderRadius: 1,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Box
|
|
97
|
+
sx={{
|
|
98
|
+
position: "absolute",
|
|
99
|
+
top: cursorPosition.y + "px",
|
|
100
|
+
left: cursorPosition.x + "px",
|
|
101
|
+
transform: "translate(-50%, -50%)",
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<ColorCursor
|
|
105
|
+
color={`hsl(${hueFromColorFormats(colorFormats)}, 100%, 50%)`}
|
|
106
|
+
/>
|
|
107
|
+
</Box>
|
|
108
|
+
</Box>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StoryFn, Meta } from "@storybook/react";
|
|
3
|
+
import { ColorPicker, ColorPickerProps } from "./color-picker";
|
|
4
|
+
import { ColorProvider } from "../../../contexts/color-context";
|
|
5
|
+
|
|
6
|
+
function ColorPickerWrapper(props: ColorPickerProps) {
|
|
7
|
+
return (
|
|
8
|
+
<ColorProvider initialColor={props.color}>
|
|
9
|
+
<ColorPicker />
|
|
10
|
+
</ColorProvider>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
/* 👇 The title prop is optional.
|
|
16
|
+
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
|
17
|
+
* to learn how to generate automatic titles
|
|
18
|
+
*/
|
|
19
|
+
title: "Slate Color Picker/Color Picker",
|
|
20
|
+
component: ColorPickerWrapper,
|
|
21
|
+
} as Meta<typeof ColorPickerWrapper>;
|
|
22
|
+
|
|
23
|
+
export const Primary = {
|
|
24
|
+
args: { color: "green" } as ColorPickerProps,
|
|
25
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Box } from "@mui/material";
|
|
3
|
+
import { ColorGradient } from "./color-gradient";
|
|
4
|
+
import { ColorHue } from "./color-hue";
|
|
5
|
+
import { ColorFormatsView } from "./color-formats-view";
|
|
6
|
+
import { ColorContext } from "../../../contexts/color-context";
|
|
7
|
+
|
|
8
|
+
export type ColorPickerProps = {
|
|
9
|
+
color: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ColorPicker() {
|
|
13
|
+
const { colorFormats, setColorFormats } = React.useContext(ColorContext);
|
|
14
|
+
const width = 360;
|
|
15
|
+
const height = 250;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
|
|
19
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
20
|
+
<ColorGradient
|
|
21
|
+
width={width}
|
|
22
|
+
height={height}
|
|
23
|
+
colorFormats={colorFormats}
|
|
24
|
+
setColorFormats={setColorFormats}
|
|
25
|
+
/>
|
|
26
|
+
<ColorHue
|
|
27
|
+
width={width}
|
|
28
|
+
colorFormats={colorFormats}
|
|
29
|
+
setColorFormats={setColorFormats}
|
|
30
|
+
/>
|
|
31
|
+
</Box>
|
|
32
|
+
<Box>
|
|
33
|
+
<ColorFormatsView
|
|
34
|
+
height={height}
|
|
35
|
+
colorFormats={colorFormats}
|
|
36
|
+
setColorFormats={setColorFormats}
|
|
37
|
+
/>
|
|
38
|
+
</Box>
|
|
39
|
+
</Box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ColorPopover, ColorPopoverProps } from "./color-popover";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { StoryFn, Meta } from "@storybook/react";
|
|
4
|
+
import { Button } from "@mui/material";
|
|
5
|
+
import { ColorProvider } from "../../../contexts/color-context";
|
|
6
|
+
|
|
7
|
+
function ColorPopoverWrapper(props: ColorPopoverProps) {
|
|
8
|
+
return (
|
|
9
|
+
<ColorProvider initialColor="blue">
|
|
10
|
+
<ColorPopover {...props} />
|
|
11
|
+
</ColorProvider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
/* 👇 The title prop is optional.
|
|
17
|
+
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
|
18
|
+
* to learn how to generate automatic titles
|
|
19
|
+
*/
|
|
20
|
+
title: "Slate Color Picker/Color Popover",
|
|
21
|
+
component: ColorPopoverWrapper,
|
|
22
|
+
} as Meta<typeof ColorPopoverWrapper>;
|
|
23
|
+
|
|
24
|
+
export const Primary = {
|
|
25
|
+
args: { children: <Button>Pick Color</Button> } as ColorPopoverProps,
|
|
26
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Popover, Box, BoxProps } from "@mui/material";
|
|
3
|
+
import { ColorPicker } from "./color-picker";
|
|
4
|
+
|
|
5
|
+
export type ColorPopoverProps = {
|
|
6
|
+
boxProps?: BoxProps;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function ColorPopover({
|
|
10
|
+
children,
|
|
11
|
+
boxProps,
|
|
12
|
+
}: React.PropsWithChildren<ColorPopoverProps>) {
|
|
13
|
+
const [anchorEl, setAnchorEl] = React.useState<HTMLDivElement | null>(null);
|
|
14
|
+
|
|
15
|
+
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
16
|
+
setAnchorEl(event.currentTarget);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const handleClose = () => {
|
|
20
|
+
setAnchorEl(null);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const open = Boolean(anchorEl);
|
|
24
|
+
const id = open ? "simple-popover" : undefined;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<Box
|
|
29
|
+
{...boxProps}
|
|
30
|
+
sx={{
|
|
31
|
+
...boxProps?.sx,
|
|
32
|
+
// "-webkit-appearance": "button",
|
|
33
|
+
cursor: "pointer",
|
|
34
|
+
// backgroundColor: "transparent",
|
|
35
|
+
// backgroundImage: "none",
|
|
36
|
+
}}
|
|
37
|
+
role="button"
|
|
38
|
+
// component="button"
|
|
39
|
+
aria-describedby={id}
|
|
40
|
+
onClick={handleClick}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</Box>
|
|
44
|
+
<Popover
|
|
45
|
+
id={id}
|
|
46
|
+
open={open}
|
|
47
|
+
anchorEl={anchorEl}
|
|
48
|
+
onClose={handleClose}
|
|
49
|
+
anchorOrigin={{
|
|
50
|
+
vertical: "top",
|
|
51
|
+
horizontal: "right",
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<ColorPicker />
|
|
55
|
+
</Popover>
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ColorSwatch, ColorSwatchProps } from "./color-swatch";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { StoryFn, Meta } from "@storybook/react";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
/* 👇 The title prop is optional.
|
|
7
|
+
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
|
8
|
+
* to learn how to generate automatic titles
|
|
9
|
+
*/
|
|
10
|
+
title: "Slate Color Picker/Color Swatch",
|
|
11
|
+
component: ColorSwatch,
|
|
12
|
+
} as Meta<typeof ColorSwatch>;
|
|
13
|
+
|
|
14
|
+
export const Primary = {
|
|
15
|
+
args: { color: "blue" } as ColorSwatchProps,
|
|
16
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Button, Popover, Typography, useTheme } from "@mui/material";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { colorConvert, isColor } from "@springmicro/utils/editor";
|
|
4
|
+
|
|
5
|
+
export type ColorSwatchProps = {
|
|
6
|
+
color: string;
|
|
7
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function ColorSwatch(props: ColorSwatchProps) {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
const { color, onClick } = props;
|
|
13
|
+
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
|
|
14
|
+
|
|
15
|
+
if (!isColor(color)) {
|
|
16
|
+
throw new Error(`Could not render color ${color}.`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
20
|
+
setAnchorEl(event.currentTarget);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handlePopoverClose = () => {
|
|
24
|
+
setAnchorEl(null);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const open = Boolean(anchorEl);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<Button
|
|
32
|
+
aria-owns={open ? "mouse-over-popover" : undefined}
|
|
33
|
+
aria-haspopup="true"
|
|
34
|
+
onMouseEnter={handlePopoverOpen}
|
|
35
|
+
onMouseLeave={handlePopoverClose}
|
|
36
|
+
aria-label={`selected color: ${colorConvert(color, "hex")}`}
|
|
37
|
+
sx={{
|
|
38
|
+
backgroundColor: color,
|
|
39
|
+
width: 24,
|
|
40
|
+
height: 24,
|
|
41
|
+
minWidth: "unset",
|
|
42
|
+
padding: "unset",
|
|
43
|
+
borderRadius: 0.5,
|
|
44
|
+
"&:hover": {
|
|
45
|
+
backgroundColor: color,
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
outline: `4px solid ${theme.palette.neutral["200"]}`,
|
|
48
|
+
},
|
|
49
|
+
}}
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
></Button>
|
|
52
|
+
<Popover
|
|
53
|
+
id="mouse-over-popover"
|
|
54
|
+
sx={{
|
|
55
|
+
pointerEvents: "none",
|
|
56
|
+
}}
|
|
57
|
+
open={open}
|
|
58
|
+
anchorEl={anchorEl}
|
|
59
|
+
anchorOrigin={{
|
|
60
|
+
vertical: "bottom",
|
|
61
|
+
horizontal: "left",
|
|
62
|
+
}}
|
|
63
|
+
transformOrigin={{
|
|
64
|
+
vertical: "top",
|
|
65
|
+
horizontal: "left",
|
|
66
|
+
}}
|
|
67
|
+
onClose={handlePopoverClose}
|
|
68
|
+
disableRestoreFocus
|
|
69
|
+
>
|
|
70
|
+
<Typography sx={{ p: 1 }} fontSize={12}>
|
|
71
|
+
{colorConvert(color, "hex")}
|
|
72
|
+
</Typography>
|
|
73
|
+
</Popover>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const defaultColors = [
|
|
2
|
+
"#000000",
|
|
3
|
+
"#e60000",
|
|
4
|
+
"#ff9900",
|
|
5
|
+
"#ffff00",
|
|
6
|
+
"#008a00",
|
|
7
|
+
"#0066cc",
|
|
8
|
+
"#9933ff",
|
|
9
|
+
"#ffffff",
|
|
10
|
+
"#f9fafc", // This is the color of the slate editor background, without this color it's hard to set the background color back to the paper color without using a tool to tell you the color of the element. I would rather offer a transparent option to solve this problem.
|
|
11
|
+
"#facccc",
|
|
12
|
+
"#ffebcc",
|
|
13
|
+
"#ffffcc",
|
|
14
|
+
"#cce8cc",
|
|
15
|
+
"#cce0f5",
|
|
16
|
+
"#ebd6ff",
|
|
17
|
+
"#bbbbbb",
|
|
18
|
+
"#f06666",
|
|
19
|
+
"#ffc266",
|
|
20
|
+
"#ffff66",
|
|
21
|
+
"#66b966",
|
|
22
|
+
"#66a3e0",
|
|
23
|
+
"#c285ff",
|
|
24
|
+
"#888888",
|
|
25
|
+
"#a10000",
|
|
26
|
+
"#b26b00",
|
|
27
|
+
"#b2b200",
|
|
28
|
+
"#006100",
|
|
29
|
+
"#0047b2",
|
|
30
|
+
"#6b24b2",
|
|
31
|
+
"#444444",
|
|
32
|
+
"#5c0000",
|
|
33
|
+
"#663d00",
|
|
34
|
+
"#666600",
|
|
35
|
+
"#003700",
|
|
36
|
+
"#002966",
|
|
37
|
+
"#3d1466",
|
|
38
|
+
];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { BaseEditor, BaseSelection, Location, Transforms } from "slate";
|
|
3
|
+
import { ColorProvider } from "../../../contexts/color-context";
|
|
4
|
+
import { ColorPopover } from "./color-popover";
|
|
5
|
+
import { activeMark, addMarkData, defaultMarkData } from "../../utils";
|
|
6
|
+
import Icon from "../../common/icon";
|
|
7
|
+
import Button from "../../common/button";
|
|
8
|
+
import { ColorSwatch } from "./color-swatch";
|
|
9
|
+
import { Box, Menu } from "@mui/material";
|
|
10
|
+
import { defaultColors } from "./default-colors";
|
|
11
|
+
import { CustomEditor } from "../../custom-types";
|
|
12
|
+
|
|
13
|
+
export type SlateColorButtonProps = {
|
|
14
|
+
format: "bgcolor" | "color";
|
|
15
|
+
editor: CustomEditor;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function SlateColorButton({ format, editor }: SlateColorButtonProps) {
|
|
19
|
+
const [anchorEl, setAnchorEl] = React.useState<null | Element>(null);
|
|
20
|
+
const open = Boolean(anchorEl);
|
|
21
|
+
const id = `${format}-menu-button`;
|
|
22
|
+
const [selection, setSelection] = React.useState<BaseSelection | Location>();
|
|
23
|
+
const initialColor =
|
|
24
|
+
format === "bgcolor"
|
|
25
|
+
? defaultMarkData["bgColor"]
|
|
26
|
+
: defaultMarkData["color"];
|
|
27
|
+
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
if (editor.selection) {
|
|
30
|
+
setSelection(editor.selection);
|
|
31
|
+
}
|
|
32
|
+
}, [editor.selection]);
|
|
33
|
+
|
|
34
|
+
const getCurrentColor = () => {
|
|
35
|
+
if (!selection) return initialColor;
|
|
36
|
+
Transforms.select(editor, selection!);
|
|
37
|
+
return activeMark(editor, format)[format] ?? initialColor;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleClick = (event?: React.MouseEvent<HTMLButtonElement>) => {
|
|
41
|
+
const el = event?.currentTarget
|
|
42
|
+
? event.currentTarget
|
|
43
|
+
: document.querySelector(`#${id}`);
|
|
44
|
+
setSelection(editor.selection);
|
|
45
|
+
if (anchorEl) setAnchorEl(null);
|
|
46
|
+
else if (el) setAnchorEl(el);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleSwatchClick = (value: string) => {
|
|
50
|
+
if (selection === undefined) return;
|
|
51
|
+
Transforms.select(editor, selection!);
|
|
52
|
+
console.log(editor.selection, format, value);
|
|
53
|
+
addMarkData(editor, {
|
|
54
|
+
format: format,
|
|
55
|
+
value: value,
|
|
56
|
+
});
|
|
57
|
+
if (anchorEl) setAnchorEl(null);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<Button
|
|
63
|
+
id={id}
|
|
64
|
+
aria-controls={open ? "basic-menu" : undefined}
|
|
65
|
+
aria-haspopup="true"
|
|
66
|
+
aria-expanded={open ? "true" : undefined}
|
|
67
|
+
onClick={handleClick}
|
|
68
|
+
format="link"
|
|
69
|
+
style={{ backgroundColor: "#f9fafc" }}
|
|
70
|
+
>
|
|
71
|
+
<Icon icon={format} />
|
|
72
|
+
</Button>
|
|
73
|
+
<Menu
|
|
74
|
+
id="basic-menu"
|
|
75
|
+
anchorEl={anchorEl}
|
|
76
|
+
open={open}
|
|
77
|
+
onClose={() => handleClick()}
|
|
78
|
+
MenuListProps={{
|
|
79
|
+
"aria-labelledby": id,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<Box
|
|
83
|
+
sx={{
|
|
84
|
+
display: "grid",
|
|
85
|
+
gap: 1,
|
|
86
|
+
gridTemplateColumns: "repeat(6, 1fr)",
|
|
87
|
+
gridTemplateRows: "repeat(6, 1fr)",
|
|
88
|
+
p: 1,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{defaultColors.map((c) => (
|
|
92
|
+
<ColorSwatch
|
|
93
|
+
onClick={(e) => handleSwatchClick(c)}
|
|
94
|
+
color={c}
|
|
95
|
+
key={c}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
98
|
+
</Box>
|
|
99
|
+
<ColorProvider
|
|
100
|
+
initialColor={initialColor}
|
|
101
|
+
colorChangeCallback={(color) => {
|
|
102
|
+
if (color !== getCurrentColor()) {
|
|
103
|
+
addMarkData(editor, {
|
|
104
|
+
format: format,
|
|
105
|
+
value: color,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<ColorPopover>
|
|
111
|
+
<Box sx={{ px: 1 }}>
|
|
112
|
+
<Icon icon="colorize" /> Custom Color{" "}
|
|
113
|
+
<Box
|
|
114
|
+
component="span"
|
|
115
|
+
sx={{
|
|
116
|
+
// background: getCurrentColor(),
|
|
117
|
+
height: 8,
|
|
118
|
+
width: 8,
|
|
119
|
+
display: "inline-block",
|
|
120
|
+
}}
|
|
121
|
+
></Box>
|
|
122
|
+
</Box>
|
|
123
|
+
</ColorPopover>
|
|
124
|
+
</ColorProvider>
|
|
125
|
+
</Menu>{" "}
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import Button from "../../common/button";
|
|
3
|
+
import Icon from "../../common/icon";
|
|
4
|
+
import { isBlockActive } from "../../utils";
|
|
5
|
+
import usePopup from "../../utils/customHooks/usePopup";
|
|
6
|
+
import { insertEmbed } from "../../utils/embed";
|
|
7
|
+
import { Transforms } from "slate";
|
|
8
|
+
import { ReactEditor } from "slate-react";
|
|
9
|
+
|
|
10
|
+
const Embed = ({ editor, format }) => {
|
|
11
|
+
const urlInputRef = useRef();
|
|
12
|
+
const [showInput, setShowInput] = usePopup(urlInputRef);
|
|
13
|
+
const [formData, setFormData] = useState({
|
|
14
|
+
url: "",
|
|
15
|
+
alt: "",
|
|
16
|
+
});
|
|
17
|
+
const [selection, setSelection] = useState();
|
|
18
|
+
const handleButtonClick = (e) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setSelection(editor.selection);
|
|
21
|
+
selection && ReactEditor.focus(editor);
|
|
22
|
+
|
|
23
|
+
setShowInput((prev) => !prev);
|
|
24
|
+
};
|
|
25
|
+
const handleFormSubmit = (e) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
|
|
28
|
+
selection && Transforms.select(editor, selection);
|
|
29
|
+
selection && ReactEditor.focus(editor);
|
|
30
|
+
|
|
31
|
+
insertEmbed(editor, { ...formData }, format);
|
|
32
|
+
setShowInput(false);
|
|
33
|
+
setFormData({
|
|
34
|
+
url: "",
|
|
35
|
+
alt: "",
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
const handleImageUpload = () => {
|
|
39
|
+
setShowInput(false);
|
|
40
|
+
};
|
|
41
|
+
return (
|
|
42
|
+
<div ref={urlInputRef} className="popup-wrapper">
|
|
43
|
+
<Button
|
|
44
|
+
active={isBlockActive(editor, format)}
|
|
45
|
+
style={{
|
|
46
|
+
border: showInput ? "1px solid lightgray" : "",
|
|
47
|
+
borderBottom: "none",
|
|
48
|
+
}}
|
|
49
|
+
format={format}
|
|
50
|
+
onClick={handleButtonClick}
|
|
51
|
+
>
|
|
52
|
+
<Icon icon={format} />
|
|
53
|
+
</Button>
|
|
54
|
+
{showInput && (
|
|
55
|
+
<div className="popup">
|
|
56
|
+
{format === "image" && (
|
|
57
|
+
<div>
|
|
58
|
+
<div
|
|
59
|
+
style={{ display: "flex", gap: "10px" }}
|
|
60
|
+
onClick={handleImageUpload}
|
|
61
|
+
>
|
|
62
|
+
<Icon icon="upload" />
|
|
63
|
+
<span>Upload</span>
|
|
64
|
+
</div>
|
|
65
|
+
<p style={{ textAlign: "center", opacity: "0.7", width: "100%" }}>
|
|
66
|
+
OR
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
<form onSubmit={handleFormSubmit}>
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
placeholder="Enter url"
|
|
74
|
+
value={formData.url}
|
|
75
|
+
onChange={(e) =>
|
|
76
|
+
setFormData((prev) => ({ ...prev, url: e.target.value }))
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
<input
|
|
80
|
+
type="text"
|
|
81
|
+
placeholder="Enter alt"
|
|
82
|
+
value={formData.alt}
|
|
83
|
+
onChange={(e) =>
|
|
84
|
+
setFormData((prev) => ({ ...prev, alt: e.target.value }))
|
|
85
|
+
}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<Button type="submit">Save</Button>
|
|
89
|
+
</form>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default Embed;
|