@flozy/editor 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintignore +4 -0
- package/.eslintrc.json +6 -0
- package/.github/workflows/npm-publish.yml +33 -0
- package/.husky/pre-commit +1 -0
- package/.storybook/main.js +20 -0
- package/.storybook/preview.js +14 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/launch.json +15 -0
- package/.vscode/settings.json +22 -0
- package/README.md +2 -0
- package/craco.config.js +16 -0
- package/package.json +107 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +43 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/components/Editor/CollaborativeEditor.js +119 -0
- package/src/components/Editor/CommonEditor.js +549 -0
- package/src/components/Editor/Editor.css +115 -0
- package/src/components/Editor/Elements/CodeToText/CodeToText.css +57 -0
- package/src/components/Editor/Elements/CodeToText/CodeToText.jsx +115 -0
- package/src/components/Editor/Elements/CodeToText/CodeToTextButton.jsx +16 -0
- package/src/components/Editor/Elements/CodeToText/HtmlCode.jsx +59 -0
- package/src/components/Editor/Elements/CodeToText/HtmlContextMenu.jsx +39 -0
- package/src/components/Editor/Elements/Color Picker/ColorPicker.css +38 -0
- package/src/components/Editor/Elements/Color Picker/ColorPicker.jsx +110 -0
- package/src/components/Editor/Elements/Color Picker/defaultColors.js +34 -0
- package/src/components/Editor/Elements/Embed/Embed.css +14 -0
- package/src/components/Editor/Elements/Embed/Embed.jsx +74 -0
- package/src/components/Editor/Elements/Embed/Image.jsx +82 -0
- package/src/components/Editor/Elements/Embed/Video.jsx +65 -0
- package/src/components/Editor/Elements/Equation/Equation.jsx +19 -0
- package/src/components/Editor/Elements/Equation/EquationButton.jsx +59 -0
- package/src/components/Editor/Elements/Equation/styles.css +4 -0
- package/src/components/Editor/Elements/Grid/Grid.js +48 -0
- package/src/components/Editor/Elements/Grid/GridButton.js +21 -0
- package/src/components/Editor/Elements/Grid/GridItem.js +57 -0
- package/src/components/Editor/Elements/ID/Id.jsx +56 -0
- package/src/components/Editor/Elements/Link/Link.jsx +24 -0
- package/src/components/Editor/Elements/Link/LinkButton.jsx +71 -0
- package/src/components/Editor/Elements/Link/styles.css +20 -0
- package/src/components/Editor/Elements/Mentions/Mentions.jsx +37 -0
- package/src/components/Editor/Elements/NewLine/NewLineButton.js +29 -0
- package/src/components/Editor/Elements/Table/Table.jsx +13 -0
- package/src/components/Editor/Elements/Table/TableSelector.css +18 -0
- package/src/components/Editor/Elements/Table/TableSelector.jsx +76 -0
- package/src/components/Editor/Elements/TableContextMenu/TableContextMenu.jsx +97 -0
- package/src/components/Editor/Elements/TableContextMenu/styles.css +18 -0
- package/src/components/Editor/RemoteCursorOverlay/Overlay.js +78 -0
- package/src/components/Editor/Toolbar/Toolbar.jsx +167 -0
- package/src/components/Editor/Toolbar/styles.css +28 -0
- package/src/components/Editor/Toolbar/toolbarGroups.js +167 -0
- package/src/components/Editor/Toolbar/toolbarIcons/align-center.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/align-left.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/align-right.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/blockquote.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/bold.png +0 -0
- package/src/components/Editor/Toolbar/toolbarIcons/fontColor.svg +4 -0
- package/src/components/Editor/Toolbar/toolbarIcons/headingOne.svg +3 -0
- package/src/components/Editor/Toolbar/toolbarIcons/headingTwo.svg +3 -0
- package/src/components/Editor/Toolbar/toolbarIcons/italic.png +0 -0
- package/src/components/Editor/Toolbar/toolbarIcons/link.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/orderedList.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/strikethrough.png +0 -0
- package/src/components/Editor/Toolbar/toolbarIcons/subscript.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/superscript.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/textColor.png +0 -0
- package/src/components/Editor/Toolbar/toolbarIcons/underline.png +0 -0
- package/src/components/Editor/Toolbar/toolbarIcons/unlink.svg +1 -0
- package/src/components/Editor/Toolbar/toolbarIcons/unorderedList.svg +1 -0
- package/src/components/Editor/YjsProvider.js +11 -0
- package/src/components/Editor/common/Button.jsx +12 -0
- package/src/components/Editor/common/Icon.jsx +82 -0
- package/src/components/Editor/common/MentionsPopup.jsx +56 -0
- package/src/components/Editor/hooks/useMentions.js +44 -0
- package/src/components/Editor/hooks/withCollaborative.js +15 -0
- package/src/components/Editor/hooks/withCommon.js +17 -0
- package/src/components/Editor/plugins/withEmbeds.js +36 -0
- package/src/components/Editor/plugins/withEquation.js +8 -0
- package/src/components/Editor/plugins/withLinks.js +9 -0
- package/src/components/Editor/plugins/withMentions.js +19 -0
- package/src/components/Editor/plugins/withTable.js +74 -0
- package/src/components/Editor/utils/SlateUtilityFunctions.js +273 -0
- package/src/components/Editor/utils/customHooks/useContextMenu.js +42 -0
- package/src/components/Editor/utils/customHooks/useFormat.js +26 -0
- package/src/components/Editor/utils/customHooks/usePopup.jsx +26 -0
- package/src/components/Editor/utils/customHooks/useResize.js +41 -0
- package/src/components/Editor/utils/draftToSlate.js +104 -0
- package/src/components/Editor/utils/embed.js +18 -0
- package/src/components/Editor/utils/equation.js +22 -0
- package/src/components/Editor/utils/events.js +56 -0
- package/src/components/Editor/utils/grid.js +12 -0
- package/src/components/Editor/utils/gridItem.js +19 -0
- package/src/components/Editor/utils/link.js +53 -0
- package/src/components/Editor/utils/mentions.js +11 -0
- package/src/components/Editor/utils/paragraph.js +4 -0
- package/src/components/Editor/utils/serializer.js +32 -0
- package/src/components/Editor/utils/table.js +151 -0
- package/src/components/index.js +5 -0
- package/src/index.js +1 -0
- package/src/stories/CollaborativeEditor.stories.js +30 -0
- package/src/stories/Editor.stories.js +24 -0
- package/src/stories/EditorSampleProps/ChatSample.js +43 -0
- package/src/stories/EditorSampleProps/LayoutOne.js +551 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
2
|
+
import "./CodeToText.css";
|
3
|
+
import Icon from "../../common/Icon";
|
4
|
+
import { Interweave } from "interweave";
|
5
|
+
import { Transforms } from "slate";
|
6
|
+
import { useSlateStatic } from "slate-react";
|
7
|
+
const CodeToText = (props) => {
|
8
|
+
const { html, action, location, handleCodeToText } = props;
|
9
|
+
const codeToTextRef = useRef();
|
10
|
+
const wrapperRef = useRef();
|
11
|
+
|
12
|
+
const editor = useSlateStatic();
|
13
|
+
const checkClick = (e) => {
|
14
|
+
const clickedComponent = e.target;
|
15
|
+
if (
|
16
|
+
wrapperRef?.current?.contains(clickedComponent) &&
|
17
|
+
!codeToTextRef?.current?.contains(clickedComponent)
|
18
|
+
) {
|
19
|
+
let partialState = {
|
20
|
+
showInput: false,
|
21
|
+
};
|
22
|
+
if (html) {
|
23
|
+
partialState.html = action === "update" ? "" : html;
|
24
|
+
}
|
25
|
+
handleCodeToText(partialState);
|
26
|
+
}
|
27
|
+
};
|
28
|
+
useEffect(() => {
|
29
|
+
document.addEventListener("click", checkClick);
|
30
|
+
return () => {
|
31
|
+
document.removeEventListener("click", checkClick);
|
32
|
+
};
|
33
|
+
}, []);
|
34
|
+
|
35
|
+
const codeOnChange = async (e) => {
|
36
|
+
// e.preventDefault();
|
37
|
+
handleCodeToText({ html: e.target.value });
|
38
|
+
};
|
39
|
+
const addHtml = () => {
|
40
|
+
if (html) {
|
41
|
+
if (action === "update") {
|
42
|
+
Transforms.setNodes(
|
43
|
+
editor,
|
44
|
+
{
|
45
|
+
html,
|
46
|
+
},
|
47
|
+
{
|
48
|
+
at: location,
|
49
|
+
}
|
50
|
+
);
|
51
|
+
} else {
|
52
|
+
Transforms.insertNodes(
|
53
|
+
editor,
|
54
|
+
{
|
55
|
+
type: "htmlCode",
|
56
|
+
html: html,
|
57
|
+
children: [{ text: "" }],
|
58
|
+
},
|
59
|
+
{
|
60
|
+
select: true,
|
61
|
+
}
|
62
|
+
);
|
63
|
+
Transforms.insertNodes(editor, {
|
64
|
+
type: "paragraph",
|
65
|
+
children: [{ text: "" }],
|
66
|
+
});
|
67
|
+
}
|
68
|
+
}
|
69
|
+
handleCodeToText({
|
70
|
+
showInput: false,
|
71
|
+
html: "",
|
72
|
+
});
|
73
|
+
};
|
74
|
+
const clearHtml = () => {
|
75
|
+
handleCodeToText({ html: "" });
|
76
|
+
};
|
77
|
+
return (
|
78
|
+
<div className="code-wrapper" ref={wrapperRef}>
|
79
|
+
<div ref={codeToTextRef} className="codeToTextWrapper">
|
80
|
+
<div className="codeToText">
|
81
|
+
<textarea
|
82
|
+
name=""
|
83
|
+
id=""
|
84
|
+
value={html}
|
85
|
+
onChange={codeOnChange}
|
86
|
+
placeholder="Write html here..."
|
87
|
+
></textarea>
|
88
|
+
<div
|
89
|
+
style={{
|
90
|
+
display: "flex",
|
91
|
+
alignItems: "center",
|
92
|
+
justifyContent: "center",
|
93
|
+
color: "white",
|
94
|
+
}}
|
95
|
+
>
|
96
|
+
<Icon icon="arrowRight" />
|
97
|
+
</div>
|
98
|
+
<div className="textOutput">
|
99
|
+
<Interweave content={html} />
|
100
|
+
</div>
|
101
|
+
</div>
|
102
|
+
<div>
|
103
|
+
<button onClick={addHtml} className="done">
|
104
|
+
Done
|
105
|
+
</button>
|
106
|
+
<button className="clear" onClick={clearHtml}>
|
107
|
+
Clear
|
108
|
+
</button>
|
109
|
+
</div>
|
110
|
+
</div>
|
111
|
+
</div>
|
112
|
+
);
|
113
|
+
};
|
114
|
+
|
115
|
+
export default CodeToText;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import Button from '../../common/Button'
|
3
|
+
import Icon from '../../common/Icon'
|
4
|
+
const CodeToTextButton = (props) => {
|
5
|
+
const {handleButtonClick} = props
|
6
|
+
|
7
|
+
return (
|
8
|
+
<>
|
9
|
+
<Button format='insert Html' onClick={() => handleButtonClick({showInput:true,action:'insert'})}>
|
10
|
+
<Icon icon='insertHtml'/>
|
11
|
+
</Button>
|
12
|
+
</>
|
13
|
+
)
|
14
|
+
}
|
15
|
+
|
16
|
+
export default CodeToTextButton
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import { Interweave } from "interweave";
|
2
|
+
import React, { useEffect } from "react";
|
3
|
+
import { Transforms, Path } from "slate";
|
4
|
+
import { useSelected, useFocused, useSlateStatic } from "slate-react";
|
5
|
+
import useFormat from "../../utils/customHooks/useFormat";
|
6
|
+
|
7
|
+
const HtmlCode = (props) => {
|
8
|
+
const { attributes, element, children } = props;
|
9
|
+
const selected = useSelected();
|
10
|
+
const focused = useFocused();
|
11
|
+
const editor = useSlateStatic();
|
12
|
+
|
13
|
+
const isHtmlEmbed = useFormat(editor, "htmlCode");
|
14
|
+
|
15
|
+
const handleKeyUp = (e) => {
|
16
|
+
if (!isHtmlEmbed) return;
|
17
|
+
if (e.keyCode === 13) {
|
18
|
+
const parentPath = Path.parent(editor.selection.focus.path);
|
19
|
+
const nextPath = Path.next(parentPath);
|
20
|
+
Transforms.insertNodes(
|
21
|
+
editor,
|
22
|
+
{
|
23
|
+
type: "paragraph",
|
24
|
+
children: [{ text: "" }],
|
25
|
+
},
|
26
|
+
{
|
27
|
+
at: nextPath,
|
28
|
+
select: true, // Focus on this node once inserted
|
29
|
+
}
|
30
|
+
);
|
31
|
+
} else if (e.keyCode === 8) {
|
32
|
+
Transforms.removeNodes(editor);
|
33
|
+
}
|
34
|
+
// console.log(e);
|
35
|
+
};
|
36
|
+
useEffect(() => {
|
37
|
+
document.addEventListener("keyup", handleKeyUp);
|
38
|
+
return () => {
|
39
|
+
document.removeEventListener("keyup", handleKeyUp);
|
40
|
+
};
|
41
|
+
}, [isHtmlEmbed]);
|
42
|
+
return (
|
43
|
+
<div
|
44
|
+
{...attributes}
|
45
|
+
{...element.attr}
|
46
|
+
style={{
|
47
|
+
boxShadow: selected && focused && "0 0 3px 3px lightgray",
|
48
|
+
marginRight: "20px",
|
49
|
+
}}
|
50
|
+
>
|
51
|
+
<div contentEditable={false}>
|
52
|
+
<Interweave content={element.html} />
|
53
|
+
</div>
|
54
|
+
{children}
|
55
|
+
</div>
|
56
|
+
);
|
57
|
+
};
|
58
|
+
|
59
|
+
export default HtmlCode;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import React, { useState } from "react";
|
2
|
+
import useContextMenu from "../../utils/customHooks/useContextMenu.js";
|
3
|
+
import Icon from "../../common/Icon.jsx";
|
4
|
+
import { Transforms, Node, Path } from "slate";
|
5
|
+
|
6
|
+
const HtmlContextMenu = (props) => {
|
7
|
+
const { editor, handleCodeToText } = props;
|
8
|
+
const [selection, setSelection] = useState();
|
9
|
+
const [showMenu, { top, left }] = useContextMenu(
|
10
|
+
editor,
|
11
|
+
"htmlCode",
|
12
|
+
setSelection
|
13
|
+
);
|
14
|
+
|
15
|
+
const handleEditHtml = () => {
|
16
|
+
Transforms.select(editor, selection);
|
17
|
+
const parentPath = Path.parent(selection.focus.path);
|
18
|
+
const htmlNode = Node.get(editor, parentPath);
|
19
|
+
handleCodeToText({
|
20
|
+
showInput: true,
|
21
|
+
html: htmlNode.html,
|
22
|
+
action: "update",
|
23
|
+
location: selection,
|
24
|
+
});
|
25
|
+
};
|
26
|
+
|
27
|
+
return (
|
28
|
+
showMenu && (
|
29
|
+
<div className="contextMenu" style={{ top, left }}>
|
30
|
+
<div className="menuOption" onClick={handleEditHtml}>
|
31
|
+
<Icon icon="pen" />
|
32
|
+
<span>Edit HTML</span>
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
)
|
36
|
+
);
|
37
|
+
};
|
38
|
+
|
39
|
+
export default HtmlContextMenu;
|
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
.color-options{
|
3
|
+
display: grid;
|
4
|
+
grid-template-columns: auto auto auto auto auto auto auto;
|
5
|
+
align-items: center;
|
6
|
+
gap: 5px;
|
7
|
+
}
|
8
|
+
.clicked{
|
9
|
+
border: 1px solid lightgray;
|
10
|
+
border-bottom: none;
|
11
|
+
}
|
12
|
+
.option,.hexPreview{
|
13
|
+
width: 16px;
|
14
|
+
height: 16px;
|
15
|
+
background-color: #000000;
|
16
|
+
|
17
|
+
}
|
18
|
+
.color-picker form{
|
19
|
+
display: flex;
|
20
|
+
align-items: center;
|
21
|
+
column-gap: 5px;
|
22
|
+
width: 100%;
|
23
|
+
}
|
24
|
+
.color-picker input{
|
25
|
+
width: 65%;
|
26
|
+
height:1.3em;
|
27
|
+
border:1px solid lightgray;
|
28
|
+
border-radius: 5px;
|
29
|
+
padding-left:5px
|
30
|
+
}
|
31
|
+
.color-picker button{
|
32
|
+
margin:0;
|
33
|
+
padding:0;
|
34
|
+
cursor: pointer;
|
35
|
+
}
|
36
|
+
.color-picker input:focus{
|
37
|
+
outline: none;
|
38
|
+
}
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import React, { useRef, useState } from "react";
|
2
|
+
import { MdFormatColorText, MdFormatColorFill, MdCheck } from "react-icons/md";
|
3
|
+
import "./ColorPicker.css";
|
4
|
+
import { colors } from "./defaultColors.js";
|
5
|
+
import { addMarkData, activeMark } from "../../utils/SlateUtilityFunctions.js";
|
6
|
+
import { Transforms } from "slate";
|
7
|
+
import usePopup from "../../utils/customHooks/usePopup";
|
8
|
+
import { ReactEditor } from "slate-react";
|
9
|
+
|
10
|
+
const logo = {
|
11
|
+
color: <MdFormatColorText size={20} />,
|
12
|
+
bgColor: <MdFormatColorFill size={20} />,
|
13
|
+
};
|
14
|
+
const ColorPicker = ({ format, editor }) => {
|
15
|
+
const [selection, setSelection] = useState();
|
16
|
+
const [hexValue, setHexValue] = useState("");
|
17
|
+
const [validHex, setValidHex] = useState();
|
18
|
+
const colorPickerRef = useRef(null);
|
19
|
+
const [showOptions, setShowOptions] = usePopup(colorPickerRef);
|
20
|
+
|
21
|
+
const isValideHexSix = new RegExp("^#[0-9A-Za-z]{6}");
|
22
|
+
const isValideHexThree = new RegExp("^#[0-9A-Za-z]{3}");
|
23
|
+
|
24
|
+
const changeColor = (e) => {
|
25
|
+
const clickedColor = e.target.getAttribute("data-value");
|
26
|
+
selection && Transforms.select(editor, selection);
|
27
|
+
selection && ReactEditor.focus(editor);
|
28
|
+
|
29
|
+
addMarkData(editor, { format, value: clickedColor });
|
30
|
+
setShowOptions(false);
|
31
|
+
};
|
32
|
+
const toggleOption = () => {
|
33
|
+
setSelection(editor.selection);
|
34
|
+
selection && ReactEditor.focus(editor);
|
35
|
+
|
36
|
+
setShowOptions((prev) => !prev);
|
37
|
+
};
|
38
|
+
const handleFormSubmit = (e) => {
|
39
|
+
e.preventDefault();
|
40
|
+
if (!validHex) return;
|
41
|
+
selection && Transforms.select(editor, selection);
|
42
|
+
|
43
|
+
addMarkData(editor, { format, value: hexValue });
|
44
|
+
setShowOptions(false);
|
45
|
+
setValidHex("");
|
46
|
+
setHexValue("");
|
47
|
+
selection && ReactEditor.focus(editor);
|
48
|
+
};
|
49
|
+
const handleHexChange = (e) => {
|
50
|
+
e.preventDefault();
|
51
|
+
const newHex = e.target.value;
|
52
|
+
setValidHex(isValideHexSix.test(newHex) || isValideHexThree.test(newHex));
|
53
|
+
setHexValue(newHex);
|
54
|
+
};
|
55
|
+
return (
|
56
|
+
<div className="color-picker popup-wrapper" ref={colorPickerRef}>
|
57
|
+
<button
|
58
|
+
style={{
|
59
|
+
color: showOptions ? "black" : activeMark(editor, format),
|
60
|
+
opacity: "1",
|
61
|
+
}}
|
62
|
+
className={showOptions ? "clicked" : ""}
|
63
|
+
onClick={toggleOption}
|
64
|
+
>
|
65
|
+
{logo[format]}
|
66
|
+
</button>
|
67
|
+
{showOptions && (
|
68
|
+
<div className="popup">
|
69
|
+
<div className="color-options">
|
70
|
+
{colors.map((color, index) => {
|
71
|
+
return (
|
72
|
+
<div
|
73
|
+
key={index}
|
74
|
+
data-value={color}
|
75
|
+
onClick={changeColor}
|
76
|
+
className="option"
|
77
|
+
style={{ background: color }}
|
78
|
+
></div>
|
79
|
+
);
|
80
|
+
})}
|
81
|
+
</div>
|
82
|
+
<p style={{ textAlign: "center", opacity: "0.7", width: "100%" }}>
|
83
|
+
OR
|
84
|
+
</p>
|
85
|
+
<form onSubmit={handleFormSubmit}>
|
86
|
+
<div
|
87
|
+
className="hexPreview"
|
88
|
+
style={{ background: validHex ? hexValue : "#000000" }}
|
89
|
+
></div>
|
90
|
+
<input
|
91
|
+
type="text"
|
92
|
+
placeholder="#000000"
|
93
|
+
value={hexValue}
|
94
|
+
onChange={handleHexChange}
|
95
|
+
style={{
|
96
|
+
border:
|
97
|
+
validHex === false ? "1px solid red" : "1px solid lightgray",
|
98
|
+
}}
|
99
|
+
/>
|
100
|
+
<button style={{ color: validHex ? "green" : "" }} type={"submit"}>
|
101
|
+
<MdCheck size={20} />
|
102
|
+
</button>
|
103
|
+
</form>
|
104
|
+
</div>
|
105
|
+
)}
|
106
|
+
</div>
|
107
|
+
);
|
108
|
+
};
|
109
|
+
|
110
|
+
export default ColorPicker;
|
@@ -0,0 +1,34 @@
|
|
1
|
+
export const colors = ["#000000","#e60000",
|
2
|
+
"#ff9900",
|
3
|
+
"#ffff00",
|
4
|
+
"#008a00",
|
5
|
+
"#0066cc",
|
6
|
+
"#9933ff",
|
7
|
+
"#ffffff",
|
8
|
+
"#facccc",
|
9
|
+
"#ffebcc",
|
10
|
+
"#ffffcc",
|
11
|
+
"#cce8cc",
|
12
|
+
"#cce0f5",
|
13
|
+
"#ebd6ff",
|
14
|
+
"#bbbbbb",
|
15
|
+
"#f06666",
|
16
|
+
"#ffc266",
|
17
|
+
"#ffff66",
|
18
|
+
"#66b966",
|
19
|
+
"#66a3e0",
|
20
|
+
"#c285ff",
|
21
|
+
"#888888",
|
22
|
+
"#a10000",
|
23
|
+
"#b26b00",
|
24
|
+
"#b2b200",
|
25
|
+
"#006100",
|
26
|
+
"#0047b2",
|
27
|
+
"#6b24b2",
|
28
|
+
"#444444",
|
29
|
+
"#5c0000",
|
30
|
+
"#663d00",
|
31
|
+
"#666600",
|
32
|
+
"#003700",
|
33
|
+
"#002966",
|
34
|
+
"#3d1466"]
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import React, {useRef, useState} from 'react';
|
2
|
+
import Button from '../../common/Button'
|
3
|
+
import Icon from '../../common/Icon'
|
4
|
+
import {isBlockActive} from '../../utils/SlateUtilityFunctions'
|
5
|
+
import usePopup from '../../utils/customHooks/usePopup'
|
6
|
+
import {insertEmbed } from '../../utils/embed.js'
|
7
|
+
import { Transforms } from 'slate';
|
8
|
+
import {ReactEditor} from 'slate-react'
|
9
|
+
|
10
|
+
import './Embed.css'
|
11
|
+
const Embed = ({editor,format}) =>{
|
12
|
+
const urlInputRef = useRef();
|
13
|
+
const [showInput,setShowInput] = usePopup(urlInputRef);
|
14
|
+
const [formData,setFormData] = useState({
|
15
|
+
url:'',
|
16
|
+
alt:''
|
17
|
+
})
|
18
|
+
const [selection,setSelection] = useState();
|
19
|
+
const handleButtonClick = (e)=>{
|
20
|
+
e.preventDefault();
|
21
|
+
setSelection(editor.selection);
|
22
|
+
selection && ReactEditor.focus(editor);
|
23
|
+
|
24
|
+
setShowInput(prev =>!prev);
|
25
|
+
}
|
26
|
+
const handleFormSubmit = (e)=>{
|
27
|
+
e.preventDefault();
|
28
|
+
|
29
|
+
selection && Transforms.select(editor,selection);
|
30
|
+
selection && ReactEditor.focus(editor);
|
31
|
+
|
32
|
+
insertEmbed(editor,{...formData},format);
|
33
|
+
setShowInput(false);
|
34
|
+
setFormData({
|
35
|
+
url:'',
|
36
|
+
alt:''
|
37
|
+
})
|
38
|
+
}
|
39
|
+
const handleImageUpload = ()=>{
|
40
|
+
setShowInput(false)
|
41
|
+
}
|
42
|
+
return (
|
43
|
+
<div ref={urlInputRef} className='popup-wrapper'>
|
44
|
+
<Button active={isBlockActive(editor,format)} style={{border: showInput?'1px solid lightgray':'',borderBottom: 'none'}} format={format} onClick={handleButtonClick}>
|
45
|
+
<Icon icon={format}/>
|
46
|
+
</Button>
|
47
|
+
{
|
48
|
+
showInput&&
|
49
|
+
<div className='popup'>
|
50
|
+
{
|
51
|
+
format === 'image' &&
|
52
|
+
<div>
|
53
|
+
<div style={{display:'flex',gap:'10px'}} onClick={handleImageUpload}>
|
54
|
+
<Icon icon='upload'/>
|
55
|
+
<span>Upload</span>
|
56
|
+
</div>
|
57
|
+
<p style={{textAlign:'center',opacity:'0.7',width:'100%'}}>OR</p>
|
58
|
+
|
59
|
+
</div>
|
60
|
+
}
|
61
|
+
<form onSubmit={handleFormSubmit}>
|
62
|
+
<input type="text" placeholder='Enter url' value={formData.url} onChange={e=>setFormData(prev =>({...prev,url:e.target.value}))}/>
|
63
|
+
<input type="text" placeholder='Enter alt' value={formData.alt} onChange={e=>setFormData(prev =>({...prev,alt:e.target.value}))}/>
|
64
|
+
|
65
|
+
|
66
|
+
<Button type='submit'>Save</Button>
|
67
|
+
</form>
|
68
|
+
</div>
|
69
|
+
}
|
70
|
+
</div>
|
71
|
+
)
|
72
|
+
}
|
73
|
+
|
74
|
+
export default Embed;
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
2
|
+
import {
|
3
|
+
useSlateStatic,
|
4
|
+
useSelected,
|
5
|
+
useFocused,
|
6
|
+
ReactEditor,
|
7
|
+
} from "slate-react";
|
8
|
+
import { Node, Transforms } from "slate";
|
9
|
+
import Icon from "../../common/Icon";
|
10
|
+
import useResize from "../../utils/customHooks/useResize.js";
|
11
|
+
|
12
|
+
const Image = ({ attributes, element, children }) => {
|
13
|
+
const { url, alt } = element;
|
14
|
+
const editor = useSlateStatic();
|
15
|
+
const selected = useSelected();
|
16
|
+
const focused = useFocused();
|
17
|
+
const [parentDOM, setParentDOM] = useState(null);
|
18
|
+
const [size, onMouseDown, resizing, onLoad] = useResize({
|
19
|
+
parentDOM,
|
20
|
+
size: element?.size,
|
21
|
+
});
|
22
|
+
const arr = Array.from(Node.elements(editor));
|
23
|
+
const ele = arr.find(([elem]) => element === elem);
|
24
|
+
|
25
|
+
useEffect(() => {
|
26
|
+
if (editor && ele[1] !== undefined) {
|
27
|
+
const dom = ReactEditor.toDOMNode(editor, Node.get(editor, ele[1]));
|
28
|
+
setParentDOM(dom);
|
29
|
+
onLoad(dom);
|
30
|
+
}
|
31
|
+
}, []);
|
32
|
+
|
33
|
+
useEffect(() => {
|
34
|
+
if (!resizing) {
|
35
|
+
Transforms.setNodes(editor, {
|
36
|
+
size: size,
|
37
|
+
});
|
38
|
+
}
|
39
|
+
}, [resizing]);
|
40
|
+
|
41
|
+
return (
|
42
|
+
<div
|
43
|
+
{...attributes}
|
44
|
+
className="embed"
|
45
|
+
style={{
|
46
|
+
display: "flex",
|
47
|
+
width: "100%",
|
48
|
+
maxWidth: "100%",
|
49
|
+
boxShadow: selected && focused && "0 0 3px 3px lightgray",
|
50
|
+
}}
|
51
|
+
{...element.attr}
|
52
|
+
>
|
53
|
+
<div
|
54
|
+
contentEditable={false}
|
55
|
+
style={{
|
56
|
+
position: "relative",
|
57
|
+
width: size.widthInPercent
|
58
|
+
? `${size.widthInPercent}%`
|
59
|
+
: `${size.width}px`,
|
60
|
+
height: `${size.height}px`,
|
61
|
+
}}
|
62
|
+
>
|
63
|
+
<img alt={alt} src={url} />
|
64
|
+
{selected && (
|
65
|
+
<button
|
66
|
+
onPointerDown={onMouseDown}
|
67
|
+
style={{
|
68
|
+
width: "15px",
|
69
|
+
height: "15px",
|
70
|
+
opacity: 1,
|
71
|
+
background: "transparent",
|
72
|
+
}}
|
73
|
+
>
|
74
|
+
<Icon icon="resize" />
|
75
|
+
</button>
|
76
|
+
)}
|
77
|
+
</div>
|
78
|
+
{children}
|
79
|
+
</div>
|
80
|
+
);
|
81
|
+
};
|
82
|
+
export default Image;
|
@@ -0,0 +1,65 @@
|
|
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.js";
|
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 { InlineMath, BlockMath } from "react-katex";
|
3
|
+
|
4
|
+
import "./styles.css";
|
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;
|