@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,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useSelected, useFocused } from "slate-react";
|
|
3
|
+
import Icon from "../../common/icon";
|
|
4
|
+
import useResize from "../../utils/customHooks/useResize";
|
|
5
|
+
|
|
6
|
+
const Image = ({ attributes, element, children }) => {
|
|
7
|
+
const { url, alt } = element;
|
|
8
|
+
const selected = useSelected();
|
|
9
|
+
const focused = useFocused();
|
|
10
|
+
const [size, onMouseDown] = useResize();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
{...attributes}
|
|
15
|
+
className="embed"
|
|
16
|
+
style={{
|
|
17
|
+
display: "flex",
|
|
18
|
+
boxShadow: selected && focused && "0 0 3px 3px lightgray",
|
|
19
|
+
}}
|
|
20
|
+
{...element.attr}
|
|
21
|
+
>
|
|
22
|
+
<div
|
|
23
|
+
contentEditable={false}
|
|
24
|
+
style={{ width: `${size.width}px`, height: `${size.height}px` }}
|
|
25
|
+
>
|
|
26
|
+
<img alt={alt} src={url} />
|
|
27
|
+
{selected && (
|
|
28
|
+
<button
|
|
29
|
+
onMouseDown={onMouseDown}
|
|
30
|
+
style={{
|
|
31
|
+
width: "15px",
|
|
32
|
+
height: "15px",
|
|
33
|
+
opacity: 1,
|
|
34
|
+
background: "transparent",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<Icon icon="resize" />
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
export default Image;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { useSelected, useFocused } from "slate-react";
|
|
3
|
+
import Icon from "../../common/icon";
|
|
4
|
+
import useResize from "../../utils/customHooks/useResize";
|
|
5
|
+
// import "./Video.css";
|
|
6
|
+
|
|
7
|
+
const Video = ({ attributes, element, children }) => {
|
|
8
|
+
const { url, alt } = element;
|
|
9
|
+
const [size, onMouseDown, resizing] = useResize();
|
|
10
|
+
const selected = useSelected();
|
|
11
|
+
const focused = useFocused();
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
{...attributes}
|
|
15
|
+
className="embed"
|
|
16
|
+
style={{
|
|
17
|
+
display: "flex",
|
|
18
|
+
boxShadow: selected && focused && "0 0 3px 3px lightgray",
|
|
19
|
+
}}
|
|
20
|
+
{...element.attr}
|
|
21
|
+
>
|
|
22
|
+
<div
|
|
23
|
+
contentEditable={false}
|
|
24
|
+
style={{ width: `${size.width}px`, height: `${size.height}px` }}
|
|
25
|
+
>
|
|
26
|
+
{
|
|
27
|
+
// The iframe reloads on each re-render and hence it stutters and the document doesn't detect mouse-up event leading to unwanted behaviour
|
|
28
|
+
// So during resize replace the iframe with a simple div
|
|
29
|
+
resizing ? (
|
|
30
|
+
<div
|
|
31
|
+
style={{
|
|
32
|
+
width: "100%",
|
|
33
|
+
height: "100%",
|
|
34
|
+
border: "2px dashed black",
|
|
35
|
+
display: "flex",
|
|
36
|
+
justifyContent: "center",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Icon icon="videoPlayer" />
|
|
41
|
+
</div>
|
|
42
|
+
) : (
|
|
43
|
+
<iframe src={url} frameBorder="0" title={alt} />
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
{selected && (
|
|
48
|
+
<button
|
|
49
|
+
onMouseDown={onMouseDown}
|
|
50
|
+
style={{
|
|
51
|
+
width: "15px",
|
|
52
|
+
height: "15px",
|
|
53
|
+
opacity: 1,
|
|
54
|
+
background: "transparent",
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<Icon icon="resize" />
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
export default Video;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { useFocused, useSelected, useSlateStatic } from "slate-react"
|
|
3
|
+
import { InlineMath, BlockMath } from "react-katex"
|
|
4
|
+
|
|
5
|
+
const Equation = ({ attributes, element, children }) => {
|
|
6
|
+
const { inline, math } = element
|
|
7
|
+
return (
|
|
8
|
+
<div className={inline ? "equation-inline" : ""}>
|
|
9
|
+
<span {...attributes} {...element.attr}>
|
|
10
|
+
<span contentEditable={false}>
|
|
11
|
+
{inline ? <InlineMath math={math} /> : <BlockMath math={math} />}
|
|
12
|
+
</span>
|
|
13
|
+
{children}
|
|
14
|
+
</span>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default Equation
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import Button from "../../common/button";
|
|
3
|
+
import Icon from "../../common/icon";
|
|
4
|
+
import usePopup from "../../utils/customHooks/usePopup";
|
|
5
|
+
import { insertEquation } from "../../utils/equation";
|
|
6
|
+
import { Transforms } from "slate";
|
|
7
|
+
|
|
8
|
+
const EquationButton = ({ editor }) => {
|
|
9
|
+
const equationInputRef = useRef(null);
|
|
10
|
+
const [showInput, setShowInput] = usePopup(equationInputRef);
|
|
11
|
+
const [math, setMath] = useState("");
|
|
12
|
+
const [displayInline, setDisplayInline] = useState(false);
|
|
13
|
+
const [selection, setSelection] = useState();
|
|
14
|
+
|
|
15
|
+
const toggleButton = () => {
|
|
16
|
+
setShowInput((prev) => !prev);
|
|
17
|
+
setDisplayInline(false);
|
|
18
|
+
setSelection(editor.selection);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleInputChange = ({ target }) => {
|
|
22
|
+
if (target.type === "checkbox") {
|
|
23
|
+
setDisplayInline((prev) => !prev);
|
|
24
|
+
} else {
|
|
25
|
+
setMath(target.value);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleAddEquation = () => {
|
|
30
|
+
if (!math) return;
|
|
31
|
+
selection && Transforms.select(editor, selection);
|
|
32
|
+
insertEquation(editor, math, displayInline);
|
|
33
|
+
console.log("btn click");
|
|
34
|
+
setShowInput(false);
|
|
35
|
+
};
|
|
36
|
+
return (
|
|
37
|
+
<div ref={equationInputRef} className="popup-wrapper">
|
|
38
|
+
<Button format="equation" onClick={toggleButton}>
|
|
39
|
+
<Icon icon="equation" />
|
|
40
|
+
</Button>
|
|
41
|
+
{showInput && (
|
|
42
|
+
<div className="popup">
|
|
43
|
+
<div style={{ display: "flex", gap: "5px" }}>
|
|
44
|
+
<input
|
|
45
|
+
type="text"
|
|
46
|
+
value={math}
|
|
47
|
+
onChange={handleInputChange}
|
|
48
|
+
placeholder="Enter formula"
|
|
49
|
+
/>
|
|
50
|
+
<div onClick={handleAddEquation}>
|
|
51
|
+
<Icon icon="add" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<label>
|
|
55
|
+
<input
|
|
56
|
+
type="checkbox"
|
|
57
|
+
checked={displayInline}
|
|
58
|
+
onChange={handleInputChange}
|
|
59
|
+
/>
|
|
60
|
+
Inline Equation
|
|
61
|
+
</label>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default EquationButton;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Range, Editor, Transforms } from "slate";
|
|
2
|
+
import React, { useRef, useState } from "react";
|
|
3
|
+
import Button from "../../common/button";
|
|
4
|
+
import Icon from "../../common/icon";
|
|
5
|
+
import usePopup from "../../utils/customHooks/usePopup";
|
|
6
|
+
const Id = ({ editor }) => {
|
|
7
|
+
const idInputRef = useRef(null);
|
|
8
|
+
const [showInput, setShowInput] = usePopup(idInputRef);
|
|
9
|
+
const [selection, setSelection] = useState();
|
|
10
|
+
const [id, setId] = useState("");
|
|
11
|
+
const toggleId = () => {
|
|
12
|
+
setSelection(editor.selection);
|
|
13
|
+
setShowInput((prev) => !prev);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const handleId = () => {
|
|
17
|
+
// selection && Transforms.select(editor,selection);
|
|
18
|
+
if (!selection || !id) return;
|
|
19
|
+
Transforms.setNodes(
|
|
20
|
+
editor,
|
|
21
|
+
{
|
|
22
|
+
attr: { id },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
at: selection,
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
setShowInput(false);
|
|
29
|
+
setId("");
|
|
30
|
+
};
|
|
31
|
+
return (
|
|
32
|
+
<div className="popup-wrapper" ref={idInputRef}>
|
|
33
|
+
<Button
|
|
34
|
+
className={showInput ? "clicked" : ""}
|
|
35
|
+
format={"add Id"}
|
|
36
|
+
onClick={toggleId}
|
|
37
|
+
>
|
|
38
|
+
<Icon icon="addId" />
|
|
39
|
+
</Button>
|
|
40
|
+
{showInput && (
|
|
41
|
+
<div className="popup" style={{ display: "flex", gap: "4px" }}>
|
|
42
|
+
<input
|
|
43
|
+
type="text"
|
|
44
|
+
placeholder="Enter an unique ID"
|
|
45
|
+
value={id}
|
|
46
|
+
onChange={(e) => setId(e.target.value)}
|
|
47
|
+
/>
|
|
48
|
+
<div onClick={handleId}>
|
|
49
|
+
<Icon icon="add" />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default Id;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { StoryFn, Meta } from "@storybook/react";
|
|
4
|
+
import Image, { ImageProps } from "./image";
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
/* 👇 The title prop is optional.
|
|
8
|
+
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
|
9
|
+
* to learn how to generate automatic titles
|
|
10
|
+
*/
|
|
11
|
+
title: "Slate/Image",
|
|
12
|
+
component: Image,
|
|
13
|
+
} as Meta<typeof Image>;
|
|
14
|
+
|
|
15
|
+
export const Primary = {
|
|
16
|
+
args: {} as ImageProps,
|
|
17
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { BaseEditor, Transforms } from "slate";
|
|
3
|
+
import {
|
|
4
|
+
ReactEditor,
|
|
5
|
+
useSlateStatic,
|
|
6
|
+
useSelected,
|
|
7
|
+
useFocused,
|
|
8
|
+
} from "slate-react";
|
|
9
|
+
import { ImageElement } from "./types";
|
|
10
|
+
import Button from "../../common/button";
|
|
11
|
+
import Icon from "../../common/icon";
|
|
12
|
+
|
|
13
|
+
export type ImageProps = {
|
|
14
|
+
attributes: any;
|
|
15
|
+
children: any;
|
|
16
|
+
element: any;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const Image = ({ attributes, children, element }: ImageProps) => {
|
|
20
|
+
const editor = useSlateStatic();
|
|
21
|
+
const path = ReactEditor.findPath(editor, element);
|
|
22
|
+
|
|
23
|
+
const selected = useSelected();
|
|
24
|
+
const focused = useFocused();
|
|
25
|
+
return (
|
|
26
|
+
<div {...attributes}>
|
|
27
|
+
{children}
|
|
28
|
+
<div
|
|
29
|
+
contentEditable={false}
|
|
30
|
+
style={{
|
|
31
|
+
position: "relative",
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<img
|
|
35
|
+
src={element.url}
|
|
36
|
+
style={{
|
|
37
|
+
display: "block",
|
|
38
|
+
maxWidth: "100%",
|
|
39
|
+
maxHeight: "20em",
|
|
40
|
+
margin: "0 auto",
|
|
41
|
+
boxShadow: selected && focused ? "0 0 0 3px #B4D5FF" : "none",
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
<Button
|
|
45
|
+
active
|
|
46
|
+
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
|
47
|
+
sx={{
|
|
48
|
+
display: selected && focused ? "inline" : "none",
|
|
49
|
+
position: "absolute",
|
|
50
|
+
top: "0.5em",
|
|
51
|
+
left: "0.5em",
|
|
52
|
+
backgroundColor: "white",
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<Icon icon="image" />
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default Image;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Box } from "@mui/material";
|
|
4
|
+
import { StoryFn, Meta } from "@storybook/react";
|
|
5
|
+
import {
|
|
6
|
+
InsertImageButtonProps,
|
|
7
|
+
InsertImageButton,
|
|
8
|
+
Element,
|
|
9
|
+
} from "./insert-image-button";
|
|
10
|
+
import { Descendant, createEditor } from "slate";
|
|
11
|
+
import { Slate, Editable } from "slate-react";
|
|
12
|
+
import withImages from "../../plugins/withImages";
|
|
13
|
+
import { withReact } from "slate-react";
|
|
14
|
+
import { allToolbarGroups } from "../../toolbar/toolbar-groups";
|
|
15
|
+
import Toolbar from "../../toolbar";
|
|
16
|
+
|
|
17
|
+
const initialValue: Descendant[] = [
|
|
18
|
+
{
|
|
19
|
+
type: "p",
|
|
20
|
+
children: [
|
|
21
|
+
{
|
|
22
|
+
text: "In addition to nodes that contain editable text, you can also create other types of nodes, like images or videos.",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "image",
|
|
28
|
+
url: "https://source.unsplash.com/kFrdX5IeQzI",
|
|
29
|
+
children: [{ text: "" }],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: "p",
|
|
33
|
+
children: [
|
|
34
|
+
{
|
|
35
|
+
text: "This example shows images in action. It features two ways to add images. You can either add an image via the toolbar icon above, or if you want in on a little secret, copy an image URL to your clipboard and paste it anywhere in the editor!",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "p",
|
|
41
|
+
children: [
|
|
42
|
+
{
|
|
43
|
+
text: "You can delete images with the cross in the top left. Try deleting this sheep:",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "image",
|
|
49
|
+
url: "https://source.unsplash.com/zOwZKwZOZq8",
|
|
50
|
+
children: [{ text: "" }],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function Wrapper() {
|
|
55
|
+
const editor = React.useMemo(() => withImages(withReact(createEditor())), []);
|
|
56
|
+
return (
|
|
57
|
+
<Slate initialValue={initialValue} editor={editor}>
|
|
58
|
+
<Box>
|
|
59
|
+
<InsertImageButton editor={editor} />
|
|
60
|
+
</Box>
|
|
61
|
+
<Box sx={{ minHeight: 400 }}>
|
|
62
|
+
<Editable
|
|
63
|
+
placeholder="Write something"
|
|
64
|
+
spellCheck={true}
|
|
65
|
+
renderElement={(props) => <Element {...props} />}
|
|
66
|
+
/>
|
|
67
|
+
</Box>
|
|
68
|
+
</Slate>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default {
|
|
73
|
+
/* 👇 The title prop is optional.
|
|
74
|
+
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
|
75
|
+
* to learn how to generate automatic titles
|
|
76
|
+
*/
|
|
77
|
+
title: "Slate/Insert Image Button",
|
|
78
|
+
component: Wrapper,
|
|
79
|
+
} as Meta<typeof Wrapper>;
|
|
80
|
+
|
|
81
|
+
export const Primary = {
|
|
82
|
+
args: {} as InsertImageButtonProps,
|
|
83
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { BaseEditor, BaseSelection, Location, Editor, Transforms } from "slate";
|
|
3
|
+
import { ImageElement } from "./types";
|
|
4
|
+
import Image from "./image";
|
|
5
|
+
import Button from "../../common/button";
|
|
6
|
+
import { isImageUrl } from "../../plugins/withImages";
|
|
7
|
+
import Icon from "../../common/icon";
|
|
8
|
+
import { useRef, useState, MouseEvent, ChangeEvent, FormEvent } from "react";
|
|
9
|
+
import { Box, Menu, MenuItem, TextField, Typography } from "@mui/material";
|
|
10
|
+
import { isBlockActive } from "../../utils";
|
|
11
|
+
import { IconButton, FormControlLabel, Checkbox } from "@mui/material";
|
|
12
|
+
import usePopup from "../../utils/customHooks/usePopup";
|
|
13
|
+
import { useShortcut } from "../../toolbar/shortcuts";
|
|
14
|
+
import { CustomEditor } from "../../custom-types";
|
|
15
|
+
|
|
16
|
+
const insertImage = (editor: Editor, url: string) => {
|
|
17
|
+
const text = { text: "" };
|
|
18
|
+
const image: ImageElement = { type: "image", url, children: [text] };
|
|
19
|
+
Transforms.insertNodes(editor, image);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Element = (props: any) => {
|
|
23
|
+
const { attributes, children, element } = props;
|
|
24
|
+
|
|
25
|
+
switch (element.type) {
|
|
26
|
+
case "image":
|
|
27
|
+
return <Image {...props} />;
|
|
28
|
+
default:
|
|
29
|
+
return <p {...attributes}>{children}</p>;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type InsertImageButtonProps = {
|
|
34
|
+
editor: CustomEditor;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function InsertImageButton(props: InsertImageButtonProps) {
|
|
38
|
+
const { editor } = props;
|
|
39
|
+
const [anchorEl, setAnchorEl] = useState<null | Element>(null);
|
|
40
|
+
const open = Boolean(anchorEl);
|
|
41
|
+
const [url, setUrl] = useState("");
|
|
42
|
+
const [selection, setSelection] = useState<BaseSelection | Location>();
|
|
43
|
+
|
|
44
|
+
const handleClick = (event?: MouseEvent<HTMLButtonElement>) => {
|
|
45
|
+
const el = event?.currentTarget
|
|
46
|
+
? event.currentTarget
|
|
47
|
+
: document.querySelector("#image-menu-button");
|
|
48
|
+
setSelection(editor.selection);
|
|
49
|
+
if (anchorEl) setAnchorEl(null);
|
|
50
|
+
else if (el) setAnchorEl(el);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// const handleClose = () => {
|
|
54
|
+
// setAnchorEl(null)
|
|
55
|
+
// }
|
|
56
|
+
|
|
57
|
+
const handleInsertImage = (e: FormEvent) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
if (selection === undefined) return;
|
|
60
|
+
Transforms.select(editor, selection!);
|
|
61
|
+
insertImage(editor, url);
|
|
62
|
+
setUrl("");
|
|
63
|
+
handleClick();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
|
|
67
|
+
setUrl(target?.value);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
useShortcut("image", () => handleClick());
|
|
71
|
+
return (
|
|
72
|
+
// <Button
|
|
73
|
+
// onClick={() => {
|
|
74
|
+
// const url = window.prompt("Enter the URL of the image:");
|
|
75
|
+
// if (url && !isImageUrl(url)) {
|
|
76
|
+
// alert("URL is not an image");
|
|
77
|
+
// return;
|
|
78
|
+
// }
|
|
79
|
+
// url && insertImage(editor, url);
|
|
80
|
+
// }}
|
|
81
|
+
// >
|
|
82
|
+
// <Icon icon="image" />
|
|
83
|
+
// </Button>
|
|
84
|
+
<>
|
|
85
|
+
<Button
|
|
86
|
+
id="image-menu-button"
|
|
87
|
+
aria-controls={open ? "basic-menu" : undefined}
|
|
88
|
+
aria-haspopup="true"
|
|
89
|
+
aria-expanded={open ? "true" : undefined}
|
|
90
|
+
onClick={handleClick}
|
|
91
|
+
format="image"
|
|
92
|
+
>
|
|
93
|
+
<Icon icon="image" />
|
|
94
|
+
</Button>
|
|
95
|
+
<Menu
|
|
96
|
+
id="basic-menu"
|
|
97
|
+
anchorEl={anchorEl}
|
|
98
|
+
open={open}
|
|
99
|
+
onClose={() => handleClick()}
|
|
100
|
+
MenuListProps={{
|
|
101
|
+
"aria-labelledby": "image-menu-button",
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<form onSubmit={handleInsertImage}>
|
|
105
|
+
<Box sx={{ display: "flex", gap: 1, px: 1 }}>
|
|
106
|
+
<TextField
|
|
107
|
+
aria-label="url"
|
|
108
|
+
type="text"
|
|
109
|
+
placeholder="https://ex.com/img.jpeg"
|
|
110
|
+
value={url}
|
|
111
|
+
onChange={handleInputChange}
|
|
112
|
+
size="small"
|
|
113
|
+
/>
|
|
114
|
+
<IconButton
|
|
115
|
+
type="submit"
|
|
116
|
+
aria-label="add image"
|
|
117
|
+
size="small"
|
|
118
|
+
disabled={!isImageUrl(url)}
|
|
119
|
+
>
|
|
120
|
+
<Icon icon="add" />
|
|
121
|
+
</IconButton>
|
|
122
|
+
</Box>
|
|
123
|
+
{!isImageUrl(url) && url.length >= 1 && (
|
|
124
|
+
<Typography color="error" variant="body1" sx={{ m: 2 }}>
|
|
125
|
+
URL is not an image
|
|
126
|
+
</Typography>
|
|
127
|
+
)}
|
|
128
|
+
</form>
|
|
129
|
+
</Menu>
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useFocused, useSelected, useSlateStatic } from "slate-react";
|
|
3
|
+
|
|
4
|
+
import { removeLink } from "../../utils/link";
|
|
5
|
+
import { Box } from "@mui/material";
|
|
6
|
+
import Icon from "../../common/icon";
|
|
7
|
+
|
|
8
|
+
const Link = ({ attributes, element, children }) => {
|
|
9
|
+
const editor = useSlateStatic();
|
|
10
|
+
const selected = useSelected();
|
|
11
|
+
const focused = useFocused();
|
|
12
|
+
return (
|
|
13
|
+
<Box
|
|
14
|
+
sx={{
|
|
15
|
+
display: "inline",
|
|
16
|
+
position: "relative",
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<a
|
|
20
|
+
href={element.href}
|
|
21
|
+
{...attributes}
|
|
22
|
+
{...element.attr}
|
|
23
|
+
target={element.target}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</a>
|
|
27
|
+
{selected && focused && (
|
|
28
|
+
<Box
|
|
29
|
+
sx={{
|
|
30
|
+
position: "absolute",
|
|
31
|
+
left: 0,
|
|
32
|
+
display: "flex",
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
backgroundColor: "white",
|
|
35
|
+
padding: 1,
|
|
36
|
+
gap: 4,
|
|
37
|
+
borderRadius: 1,
|
|
38
|
+
border: "1px solid lightgray",
|
|
39
|
+
width: "fit-content",
|
|
40
|
+
zIndex: 1,
|
|
41
|
+
}}
|
|
42
|
+
contentEditable={false}
|
|
43
|
+
>
|
|
44
|
+
<a href={element.href} target={element.target}>
|
|
45
|
+
{element.href}
|
|
46
|
+
</a>
|
|
47
|
+
<button onClick={() => removeLink(editor)}>
|
|
48
|
+
<Icon icon="unlink" />
|
|
49
|
+
</button>
|
|
50
|
+
</Box>
|
|
51
|
+
)}
|
|
52
|
+
</Box>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default Link;
|