@markopolo_ai_inc/markopolo-email-editor 1.0.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 (91) hide show
  1. package/README.md +63 -0
  2. package/build/asset-manifest.json +12 -0
  3. package/build/favicon.ico +0 -0
  4. package/build/index.html +1 -0
  5. package/build/logo192.png +0 -0
  6. package/build/logo512.png +0 -0
  7. package/build/manifest.json +25 -0
  8. package/build/robots.txt +3 -0
  9. package/build/static/css/main.588cb535.css +9 -0
  10. package/build/static/js/206.a4343501.chunk.js +1 -0
  11. package/build/static/js/main.053d366a.js +2 -0
  12. package/build/static/js/main.053d366a.js.LICENSE.txt +56 -0
  13. package/package.json +64 -0
  14. package/public/favicon.ico +0 -0
  15. package/public/index.html +50 -0
  16. package/public/logo192.png +0 -0
  17. package/public/logo512.png +0 -0
  18. package/public/manifest.json +25 -0
  19. package/public/robots.txt +3 -0
  20. package/src/App.js +15 -0
  21. package/src/components/EmailEditor/assets/App.css +339 -0
  22. package/src/components/EmailEditor/assets/Columns.css +309 -0
  23. package/src/components/EmailEditor/assets/Header.css +174 -0
  24. package/src/components/EmailEditor/assets/ImageBlock.css +12 -0
  25. package/src/components/EmailEditor/assets/Preview.css +30 -0
  26. package/src/components/EmailEditor/assets/RichText.css +199 -0
  27. package/src/components/EmailEditor/assets/RightSettings.css +520 -0
  28. package/src/components/EmailEditor/assets/Sidebar.css +195 -0
  29. package/src/components/EmailEditor/components/BlockItems/ButtonBlock.js +25 -0
  30. package/src/components/EmailEditor/components/BlockItems/DividerBlock.js +19 -0
  31. package/src/components/EmailEditor/components/BlockItems/HeadingBlock.js +16 -0
  32. package/src/components/EmailEditor/components/BlockItems/ImageBlock.js +28 -0
  33. package/src/components/EmailEditor/components/BlockItems/MenuBlock.js +52 -0
  34. package/src/components/EmailEditor/components/BlockItems/SocialLinkBlocks.js +26 -0
  35. package/src/components/EmailEditor/components/BlockItems/SpacerBlock.js +23 -0
  36. package/src/components/EmailEditor/components/BlockItems/TextBlock.js +16 -0
  37. package/src/components/EmailEditor/components/BlockItems/index.js +25 -0
  38. package/src/components/EmailEditor/components/ColorPicker/index.js +26 -0
  39. package/src/components/EmailEditor/components/Column/index.js +253 -0
  40. package/src/components/EmailEditor/components/Header/index.js +243 -0
  41. package/src/components/EmailEditor/components/LeftSideBar/index.js +253 -0
  42. package/src/components/EmailEditor/components/Main/index.js +281 -0
  43. package/src/components/EmailEditor/components/Preview/index.js +97 -0
  44. package/src/components/EmailEditor/components/RichText/Bold.js +37 -0
  45. package/src/components/EmailEditor/components/RichText/FontColor.js +39 -0
  46. package/src/components/EmailEditor/components/RichText/InsertOrderedList.js +36 -0
  47. package/src/components/EmailEditor/components/RichText/InsertUnorderedList.js +36 -0
  48. package/src/components/EmailEditor/components/RichText/Italic.js +36 -0
  49. package/src/components/EmailEditor/components/RichText/Link.js +99 -0
  50. package/src/components/EmailEditor/components/RichText/RichTextLayout.js +53 -0
  51. package/src/components/EmailEditor/components/RichText/Strikethrough.js +36 -0
  52. package/src/components/EmailEditor/components/RichText/TextAlign.js +58 -0
  53. package/src/components/EmailEditor/components/RichText/Underline.js +36 -0
  54. package/src/components/EmailEditor/components/RichText/index.js +210 -0
  55. package/src/components/EmailEditor/components/RightSetting/index.js +126 -0
  56. package/src/components/EmailEditor/components/StyleSettings/ButtonStyleSettings.js +141 -0
  57. package/src/components/EmailEditor/components/StyleSettings/ColumnStyleSettings.js +241 -0
  58. package/src/components/EmailEditor/components/StyleSettings/DividerStyleSettings.js +111 -0
  59. package/src/components/EmailEditor/components/StyleSettings/HeadingStyleSettings.js +162 -0
  60. package/src/components/EmailEditor/components/StyleSettings/ImageStyleSettings.js +217 -0
  61. package/src/components/EmailEditor/components/StyleSettings/MenuStyleSettings.js +177 -0
  62. package/src/components/EmailEditor/components/StyleSettings/PaddingSettings.js +30 -0
  63. package/src/components/EmailEditor/components/StyleSettings/SocialLinkSettings.js +250 -0
  64. package/src/components/EmailEditor/components/StyleSettings/SpacerStyleSettings.js +38 -0
  65. package/src/components/EmailEditor/components/StyleSettings/TextStyleSettings.js +108 -0
  66. package/src/components/EmailEditor/components/StyleSettings/index.js +32 -0
  67. package/src/components/EmailEditor/configs/getBlockConfigsList.js +263 -0
  68. package/src/components/EmailEditor/configs/getColumnConfigFunc.js +59 -0
  69. package/src/components/EmailEditor/configs/getColumnsSettings.js +246 -0
  70. package/src/components/EmailEditor/configs/useDataSource.js +19 -0
  71. package/src/components/EmailEditor/index.js +93 -0
  72. package/src/components/EmailEditor/reducers/index.js +173 -0
  73. package/src/components/EmailEditor/translation/en.js +166 -0
  74. package/src/components/EmailEditor/translation/index.js +39 -0
  75. package/src/components/EmailEditor/translation/zh.js +166 -0
  76. package/src/components/EmailEditor/utils/classNames.js +5 -0
  77. package/src/components/EmailEditor/utils/dataToHTML.js +323 -0
  78. package/src/components/EmailEditor/utils/exportValidation.js +296 -0
  79. package/src/components/EmailEditor/utils/helpers.js +48 -0
  80. package/src/components/EmailEditor/utils/pexels.js +20 -0
  81. package/src/components/EmailEditor/utils/useSection.js +24 -0
  82. package/src/components/EmailEditor/utils/useStyleLayout.js +82 -0
  83. package/src/index.css +99 -0
  84. package/src/index.js +15 -0
  85. package/src/logo.svg +1 -0
  86. package/src/pages/AppPage/index.js +10 -0
  87. package/src/pages/Dashboard/Header.js +192 -0
  88. package/src/pages/Dashboard/defaultBlockList.json +1758 -0
  89. package/src/pages/Dashboard/index.js +48 -0
  90. package/src/reportWebVitals.js +13 -0
  91. package/src/setupTests.js +5 -0
@@ -0,0 +1,281 @@
1
+ import { useContext, useCallback, useEffect } from "react";
2
+ import { GlobalContext } from "../../reducers";
3
+ import { throttle, deepClone } from "../../utils/helpers";
4
+
5
+ import LeftSideBar from "../LeftSideBar";
6
+ import Preview from "../Preview";
7
+ import RightSetting from "../RightSetting";
8
+ import useTranslation from "../../translation";
9
+ import useDataSource from "../../configs/useDataSource";
10
+
11
+ const Main = ({ language }) => {
12
+ const { blockList, setBlockList, currentItem, setCurrentItem, setIsDragStart, setLanguage } = useContext(GlobalContext);
13
+ const { t } = useTranslation();
14
+ const { getColumnConfig } = useDataSource();
15
+
16
+ const defaultContentConfig = {
17
+ name: t("drag_block_here"),
18
+ key: "empty",
19
+ width: "100%",
20
+ styles: {
21
+ desktop: {
22
+ backgroundColor: "transparent",
23
+ paddingTop: 0,
24
+ paddingLeft: 0,
25
+ paddingRight: 0,
26
+ paddingBottom: 0,
27
+ },
28
+ mobile: {},
29
+ },
30
+ };
31
+
32
+ useEffect(() => {
33
+ setLanguage(language);
34
+ }, [language, setLanguage]);
35
+
36
+ // Deselect current item
37
+ const blurCurrentItem = (event) => {
38
+ event.preventDefault();
39
+ event.stopPropagation();
40
+ setCurrentItem(null);
41
+ };
42
+ const clearLabelStyles = () => {
43
+ const dragLabelElements = document.getElementsByClassName("block-drag-label-content");
44
+ Array.from(dragLabelElements).forEach((item) => {
45
+ item.children[0].style.visibility = "hidden";
46
+ });
47
+ };
48
+
49
+ const clearContentLabelStyles = () => {
50
+ const dragContentLabelElements = document.getElementsByClassName("block-content-drag-label-content");
51
+ Array.from(dragContentLabelElements).forEach((item) => {
52
+ item.children[0].style.visibility = "hidden";
53
+ });
54
+ };
55
+
56
+ // throttle() returns a new function reference; deps are intentionally minimal
57
+ // eslint-disable-next-line react-hooks/exhaustive-deps
58
+ const onDragOver = useCallback(
59
+ throttle((event) => {
60
+ event.preventDefault();
61
+ event.stopPropagation();
62
+ let { index } = event.target.dataset;
63
+ switch (event.target.dataset.type) {
64
+ case "empty-block":
65
+ clearLabelStyles();
66
+ clearContentLabelStyles();
67
+ event.target.style.border = "1px dashed #2faade";
68
+ break;
69
+
70
+ case "drag-over-column":
71
+ clearContentLabelStyles();
72
+ const dragLabelElements = document.getElementsByClassName("block-drag-label-content");
73
+ Array.from(dragLabelElements).forEach((item) => {
74
+ if (Number(item.dataset.index) === Number(index)) {
75
+ item.children[0].style.visibility = "visible";
76
+ } else {
77
+ item.children[0].style.visibility = "hidden";
78
+ }
79
+ });
80
+ break;
81
+
82
+ case "block-item-move":
83
+ clearLabelStyles();
84
+ const dragBlockItemElements = document.getElementsByClassName("block-content-drag-label-content");
85
+ Array.from(dragBlockItemElements).forEach((item) => {
86
+ if (item.dataset.index === index) {
87
+ item.children[0].style.visibility = "visible";
88
+ } else {
89
+ item.children[0].style.visibility = "hidden";
90
+ }
91
+ });
92
+ break;
93
+
94
+ case "empty-block-item":
95
+ clearLabelStyles();
96
+ clearContentLabelStyles();
97
+ const parentNode = event.target.parentNode;
98
+ parentNode && parentNode.classList.contains("block-empty-content") && (parentNode.style.outlineStyle = "solid");
99
+ break;
100
+ default:
101
+ clearLabelStyles();
102
+ clearContentLabelStyles();
103
+ break;
104
+ }
105
+ }, 30),
106
+ []
107
+ );
108
+
109
+ const onDrop = (event) => {
110
+ event.preventDefault();
111
+ event.stopPropagation();
112
+ const { type } = event.target.dataset;
113
+ setIsDragStart(false);
114
+ switch (type) {
115
+ // First time adding an element
116
+ case "empty-block":
117
+ const newCurrentItem = currentItem.data.key !== "column" ? getColumnConfig(currentItem.data) : getColumnConfig();
118
+ setBlockList([newCurrentItem], "add");
119
+ setCurrentItem({ data: newCurrentItem, type: "edit", index: 0 });
120
+ break;
121
+ case "empty-block-item":
122
+ clearEmptyContentStyles();
123
+ const { index } = event.target.dataset;
124
+ const newBlockList = deepClone(blockList);
125
+ const indexArr = index.split("-");
126
+ const blockIndex = indexArr[0];
127
+ const itemIndex = indexArr[1];
128
+ newBlockList[blockIndex].children[itemIndex].children = [currentItem.data];
129
+ if (currentItem.type === "move") {
130
+ // Remove original element; if children becomes empty after removal, add empty content
131
+ const { index: oldIndex } = currentItem;
132
+ const oldIndexArr = oldIndex.split("-");
133
+ const oldBlockIndex = oldIndexArr[0];
134
+ const oldItemIndex = oldIndexArr[1];
135
+ const oldItem = newBlockList[oldBlockIndex].children[oldItemIndex];
136
+ if (oldItem.children.length === 1) {
137
+ newBlockList[oldBlockIndex].children[oldItemIndex].children = [defaultContentConfig];
138
+ } else {
139
+ newBlockList[oldBlockIndex].children[oldItemIndex].children = oldItem.children.filter((item, index) => index !== Number(oldIndexArr[2]));
140
+ }
141
+ }
142
+ setCurrentItem({ ...currentItem, type: "edit", index });
143
+ setBlockList([...newBlockList], "move");
144
+ break;
145
+ case "drag-over-column":
146
+ {
147
+ const { position, index } = event.target.dataset;
148
+ const newBlockList = deepClone(blockList);
149
+ let newCurrentItem = deepClone(currentItem);
150
+ if (currentItem.type === "add") {
151
+ newBlockList.splice(index, 0, currentItem.data);
152
+ newCurrentItem = { ...currentItem, type: "edit", index };
153
+ } else if (currentItem.type === "move") {
154
+ const moveItem = newBlockList.splice(currentItem.index, 1)[0];
155
+ newBlockList.splice(index, 0, moveItem);
156
+ newCurrentItem = { ...currentItem, type: "edit", index: position === "top" ? Number(index) : index - 1 };
157
+ }
158
+ setCurrentItem({ ...newCurrentItem });
159
+ setBlockList([...newBlockList], "move");
160
+ }
161
+
162
+ setTimeout(() => {
163
+ const dragLabelElements = document.getElementsByClassName("block-drag-label-content");
164
+ Array.from(dragLabelElements).forEach((item) => {
165
+ item.children[0].style.visibility = "hidden";
166
+ });
167
+ }, 30);
168
+
169
+ break;
170
+ case "block-item-move":
171
+ {
172
+ const { position, index } = event.target.dataset;
173
+ const newBlockList = deepClone(blockList);
174
+ const indexArr = index.split("-");
175
+ const blockIndex = indexArr[0];
176
+ const contentIndex = indexArr[1];
177
+ const itemIndex = indexArr[2];
178
+ const blockItem = newBlockList[blockIndex].children[contentIndex].children;
179
+ let newCurrentItem = deepClone(currentItem);
180
+ if (currentItem.type === "add") {
181
+ blockItem.splice(itemIndex, 0, currentItem.data);
182
+ newCurrentItem = { ...currentItem, type: "edit", index };
183
+ }
184
+ if (currentItem.type === "move") {
185
+ const oldIndexArr = currentItem.index.split("-");
186
+ const oldBlockIndex = oldIndexArr[0];
187
+ const oldContentIndex = oldIndexArr[1];
188
+ const oldItemIndex = oldIndexArr[2];
189
+ const oldItem = newBlockList[oldBlockIndex].children[oldContentIndex].children;
190
+ // Move currentItem.data: may move within current oldItem or to another oldItem
191
+ if (oldBlockIndex === blockIndex && oldContentIndex === contentIndex) {
192
+ // Moving within the same block item
193
+
194
+ if (oldItemIndex < itemIndex) {
195
+ const moveItem = oldItem.splice(oldItemIndex, 1)[0];
196
+ blockItem.splice(position === "top" ? Number(itemIndex) : itemIndex - 1, 0, moveItem);
197
+ newCurrentItem = {
198
+ ...currentItem,
199
+ type: "edit",
200
+ index: `${blockIndex}-${contentIndex}-${position === "top" ? Number(itemIndex) : itemIndex - 1}`,
201
+ };
202
+ } else {
203
+ const moveItem = oldItem.splice(oldItemIndex, 1)[0];
204
+ blockItem.splice(position === "top" ? Number(itemIndex) : itemIndex, 0, moveItem);
205
+ newCurrentItem = {
206
+ ...currentItem,
207
+ type: "edit",
208
+ index: `${blockIndex}-${contentIndex}-${position === "top" ? Number(itemIndex) : itemIndex}`,
209
+ };
210
+ }
211
+ } else {
212
+ // Moving to a different block item: add to new block item and remove from old; if old children becomes empty, add empty content
213
+ const moveItem = oldItem.splice(oldItemIndex, 1)[0];
214
+ blockItem.splice(position === "top" ? Number(itemIndex) : itemIndex, 0, moveItem);
215
+
216
+ if (oldItem.length === 0) {
217
+ newBlockList[oldBlockIndex].children[oldContentIndex].children = [defaultContentConfig];
218
+ }
219
+ newCurrentItem = {
220
+ ...currentItem,
221
+ type: "edit",
222
+ index: `${blockIndex}-${contentIndex}-${position === "top" ? Number(itemIndex) : itemIndex}`,
223
+ };
224
+ }
225
+ }
226
+ setCurrentItem({ ...newCurrentItem });
227
+ setBlockList([...newBlockList], "move");
228
+ }
229
+ setTimeout(() => {
230
+ const dragBlockItemElements = document.getElementsByClassName("block-content-drag-label-content");
231
+ Array.from(dragBlockItemElements).forEach((item) => {
232
+ item.children[0].style.visibility = "hidden";
233
+ });
234
+ }, 30);
235
+ break;
236
+ default:
237
+ break;
238
+ }
239
+ };
240
+
241
+ const clearEmptyContentStyles = () => {
242
+ document.querySelectorAll(".block-empty-content").forEach((item) => {
243
+ item.style.outlineStyle = "";
244
+ });
245
+ };
246
+
247
+ const clearStyles = () => {
248
+ clearLabelStyles();
249
+ clearContentLabelStyles();
250
+ clearEmptyContentStyles();
251
+ };
252
+
253
+ const onDragLeave = (event) =>
254
+ setTimeout(() => {
255
+ switch (event.target.dataset.type) {
256
+ case "empty-block":
257
+ event.target.style.border = "";
258
+ break;
259
+ case "empty-block-item":
260
+ event.target.parentNode && (event.target.parentNode.style.outlineStyle = "");
261
+ break;
262
+ case "drag-over-column":
263
+ default:
264
+ break;
265
+ }
266
+ }, 50);
267
+
268
+ return (
269
+ <>
270
+ <div className="email-editor" onDragOver={onDragOver} onDrop={onDrop} onDragLeave={onDragLeave}>
271
+ <div className="email-editor-main" onClick={blurCurrentItem}>
272
+ <LeftSideBar clearStyles={clearStyles} />
273
+ <Preview clearStyles={clearStyles} />
274
+ <RightSetting />
275
+ </div>
276
+ </div>
277
+ </>
278
+ );
279
+ };
280
+
281
+ export default Main;
@@ -0,0 +1,97 @@
1
+ import { Fragment, useContext, useEffect } from "react";
2
+ import { GlobalContext } from "../../reducers";
3
+ import Column from "../Column";
4
+ import { throttle } from "../../utils/helpers";
5
+ import Header from "../Header";
6
+ import useTranslation from "../../translation";
7
+
8
+ const Preview = (props) => {
9
+ const { clearStyles } = props;
10
+ const { t } = useTranslation();
11
+ const { previewMode, bodySettings, blockList, setSelectionRange } =
12
+ useContext(GlobalContext);
13
+
14
+ useEffect(() => {
15
+ const onSelectionChange = throttle(() => {
16
+ try {
17
+ const section = window.getSelection();
18
+ if (section) {
19
+ // section node !== document
20
+ const range = section.getRangeAt(0);
21
+ setSelectionRange(range);
22
+ }
23
+ } catch (error) {
24
+ console.warn(error);
25
+ }
26
+ }, 100);
27
+
28
+ document.addEventListener("selectionchange", onSelectionChange);
29
+
30
+ return () => {
31
+ document.removeEventListener("selectionchange", onSelectionChange);
32
+ };
33
+ }, [setSelectionRange]);
34
+
35
+ const preventDefault = (event) => {
36
+ event.preventDefault();
37
+ };
38
+
39
+ return (
40
+ <div className="preview-main">
41
+ <Header />
42
+ <div className="default-scrollbar" id="preview">
43
+ <div
44
+ className="preview-content"
45
+ style={{
46
+ width: previewMode === "desktop" ? "100%" : 364,
47
+ ...bodySettings.styles,
48
+ }}
49
+ >
50
+ <div
51
+ className="margin-auto"
52
+ style={{ maxWidth: "100%", width: "100%" }}
53
+ >
54
+ {blockList.length ? (
55
+ <>
56
+ {blockList.map((block, index) => {
57
+ return (
58
+ <Fragment key={index}>
59
+ <Column
60
+ block={block}
61
+ blockIndex={index}
62
+ clearStyles={clearStyles}
63
+ />
64
+ </Fragment>
65
+ );
66
+ })}
67
+ <div
68
+ className="relative block-drag-label-content"
69
+ data-index={blockList.length}
70
+ data-position="bottom"
71
+ >
72
+ <div className="absolute block-move-bottom">
73
+ <span className="block-tools-drag_here">
74
+ {t("drag_block_here")}
75
+ </span>
76
+ </div>
77
+ </div>
78
+ </>
79
+ ) : (
80
+ <div
81
+ data-name="dragEmpty"
82
+ className="start-to-add"
83
+ style={{ width: bodySettings.contentWidth, maxWidth: "100%" }}
84
+ data-type="empty-block"
85
+ onDragOver={preventDefault}
86
+ >
87
+ {t("add_blocks")}
88
+ </div>
89
+ )}
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ );
95
+ };
96
+
97
+ export default Preview;
@@ -0,0 +1,37 @@
1
+ import { useMemo, useContext } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faBold } from "@fortawesome/free-solid-svg-icons";
4
+ import useSection from "../../utils/useSection";
5
+ import classNames from "../../utils/classNames";
6
+
7
+ import { GlobalContext } from "../../reducers";
8
+ import useTranslation from "../../translation";
9
+
10
+ const Bold = ({ modifyText, setTextContent }) => {
11
+ const { selectionRange } = useContext(GlobalContext);
12
+ const { getSelectionNode } = useSection();
13
+ const { t } = useTranslation();
14
+
15
+ const node = useMemo(() => {
16
+ if (selectionRange) {
17
+ return getSelectionNode(selectionRange.commonAncestorContainer, "b");
18
+ } else {
19
+ return null;
20
+ }
21
+ }, [selectionRange, getSelectionNode]);
22
+
23
+ return (
24
+ <button
25
+ className={classNames("rich-text-tools-button ", node && "rich-text-tools-button-active")}
26
+ title={t("tooltip_bold")}
27
+ onClick={() => {
28
+ modifyText("bold", false, null);
29
+ setTextContent();
30
+ }}
31
+ >
32
+ <FontAwesomeIcon icon={faBold} className="rich-text-tools-button-icon" />
33
+ </button>
34
+ );
35
+ };
36
+
37
+ export default Bold;
@@ -0,0 +1,39 @@
1
+ import { useState } from "react";
2
+ import { Popover } from "antd";
3
+ import { ChromePicker } from "react-color";
4
+
5
+ const FontColor = ({ modifyText, setTextContent }) => {
6
+ const [color, setColor] = useState("#000");
7
+ const [open, setOpen] = useState(false);
8
+
9
+ const handleOpenChange = (newOpen) => {
10
+ setOpen(newOpen);
11
+ };
12
+
13
+ return (
14
+ <Popover
15
+ zIndex={1070}
16
+ content={
17
+ <div className="select-none">
18
+ <ChromePicker
19
+ color={color}
20
+ style={{ boxShadow: "none" }}
21
+ onChange={(color) => {
22
+ setColor(color.hex);
23
+ modifyText("ForeColor", false, color.hex);
24
+ }}
25
+ />
26
+ </div>
27
+ }
28
+ trigger="click"
29
+ open={open}
30
+ onOpenChange={handleOpenChange}
31
+ >
32
+ <button className="rich_text-font_color">
33
+ <div className="rich_text-font_color-content" style={{ background: color }}></div>
34
+ </button>
35
+ </Popover>
36
+ );
37
+ };
38
+
39
+ export default FontColor;
@@ -0,0 +1,36 @@
1
+ import { useMemo, useContext } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faListOl } from "@fortawesome/free-solid-svg-icons";
4
+ import useSection from "../../utils/useSection";
5
+ import classNames from "../../utils/classNames";
6
+ import { GlobalContext } from "../../reducers";
7
+ import useTranslation from "../../translation";
8
+
9
+ const InsertOrderedList = ({ modifyText, setTextContent }) => {
10
+ const { selectionRange } = useContext(GlobalContext);
11
+ const { getSelectionNode } = useSection();
12
+ const { t } = useTranslation();
13
+
14
+ const node = useMemo(() => {
15
+ if (selectionRange) {
16
+ return getSelectionNode(selectionRange.commonAncestorContainer, "ol");
17
+ } else {
18
+ return null;
19
+ }
20
+ }, [selectionRange, getSelectionNode]);
21
+
22
+ return (
23
+ <button
24
+ className={classNames("rich-text-tools-button ", node && "rich-text-tools-button-active")}
25
+ title={t("tooltip_ordered_list")}
26
+ onClick={() => {
27
+ modifyText("insertOrderedList", false, null);
28
+ setTextContent();
29
+ }}
30
+ >
31
+ <FontAwesomeIcon icon={faListOl} className="rich-text-tools-button-icon" />
32
+ </button>
33
+ );
34
+ };
35
+
36
+ export default InsertOrderedList;
@@ -0,0 +1,36 @@
1
+ import { useMemo, useContext } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faListUl } from "@fortawesome/free-solid-svg-icons";
4
+ import useSection from "../../utils/useSection";
5
+ import classNames from "../../utils/classNames";
6
+ import { GlobalContext } from "../../reducers";
7
+ import useTranslation from "../../translation";
8
+
9
+ const InsertUnorderedList = ({ modifyText, setTextContent }) => {
10
+ const { selectionRange } = useContext(GlobalContext);
11
+ const { getSelectionNode } = useSection();
12
+ const { t } = useTranslation();
13
+
14
+ const node = useMemo(() => {
15
+ if (selectionRange) {
16
+ return getSelectionNode(selectionRange.commonAncestorContainer, "ul");
17
+ } else {
18
+ return null;
19
+ }
20
+ }, [selectionRange, getSelectionNode]);
21
+
22
+ return (
23
+ <button
24
+ className={classNames("rich-text-tools-button ", node && "rich-text-tools-button-active")}
25
+ title={t("tooltip_unordered_list")}
26
+ onClick={() => {
27
+ modifyText("insertUnorderedList", false, null);
28
+ setTextContent();
29
+ }}
30
+ >
31
+ <FontAwesomeIcon icon={faListUl} className="rich-text-tools-button-icon" />
32
+ </button>
33
+ );
34
+ };
35
+
36
+ export default InsertUnorderedList;
@@ -0,0 +1,36 @@
1
+ import { useMemo, useContext } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faItalic } from "@fortawesome/free-solid-svg-icons";
4
+ import useSection from "../../utils/useSection";
5
+ import classNames from "../../utils/classNames";
6
+ import { GlobalContext } from "../../reducers";
7
+ import useTranslation from "../../translation";
8
+
9
+ const Italic = ({ modifyText, setTextContent }) => {
10
+ const { selectionRange } = useContext(GlobalContext);
11
+ const { getSelectionNode } = useSection();
12
+ const { t } = useTranslation();
13
+
14
+ const node = useMemo(() => {
15
+ if (selectionRange) {
16
+ return getSelectionNode(selectionRange.commonAncestorContainer, "i");
17
+ } else {
18
+ return null;
19
+ }
20
+ }, [selectionRange, getSelectionNode]);
21
+
22
+ return (
23
+ <button
24
+ className={classNames("rich-text-tools-button ", node && "rich-text-tools-button-active")}
25
+ title={t("tooltip_italic")}
26
+ onClick={() => {
27
+ modifyText("italic", false, null);
28
+ setTextContent();
29
+ }}
30
+ >
31
+ <FontAwesomeIcon icon={faItalic} className="rich-text-tools-button-icon" />
32
+ </button>
33
+ );
34
+ };
35
+
36
+ export default Italic;
@@ -0,0 +1,99 @@
1
+ import { useMemo, useContext, useState } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faLink, faUnlink } from "@fortawesome/free-solid-svg-icons";
4
+ import useSection from "../../utils/useSection";
5
+ import classNames from "../../utils/classNames";
6
+ import { GlobalContext } from "../../reducers";
7
+ import { Modal, Input } from "antd";
8
+ import useTranslation from "../../translation";
9
+
10
+ const Link = ({ modifyText, setTextContent }) => {
11
+ const { selectionRange } = useContext(GlobalContext);
12
+ const { t } = useTranslation();
13
+ const { getSelectionNode } = useSection();
14
+ const [isModalOpen, setIsModalOpen] = useState(false);
15
+ const [inputConfig, setInputConfig] = useState({
16
+ value: "",
17
+ range: null,
18
+ });
19
+
20
+ const node = useMemo(() => {
21
+ if (selectionRange) {
22
+ return getSelectionNode(selectionRange.commonAncestorContainer, "a");
23
+ } else {
24
+ return null;
25
+ }
26
+ }, [selectionRange, getSelectionNode]);
27
+
28
+ const addLink = () => {
29
+ const { range, value, rangeIsLink } = inputConfig;
30
+ if (rangeIsLink) {
31
+ range.commonAncestorContainer.parentNode.href = value;
32
+ } else {
33
+ let link = document.createElement("a");
34
+ link.target = "_black";
35
+ link.href = value;
36
+ // Wrap selected text with <a> tag
37
+ range.surroundContents(link);
38
+ }
39
+
40
+ setTextContent();
41
+ closeModal();
42
+ };
43
+
44
+ const closeModal = () => {
45
+ setIsModalOpen(false);
46
+ setInputConfig({ value: "", range: null });
47
+ };
48
+
49
+ const addLinkTag = () => {
50
+ let selection = window.getSelection();
51
+ let range = selection.getRangeAt(0);
52
+ const rangeParentNode = range.commonAncestorContainer.parentNode;
53
+ const rangeIsLink = rangeParentNode.nodeName === "A";
54
+ const newInputConfig = { ...inputConfig, range };
55
+ if (rangeIsLink) {
56
+ newInputConfig.rangeIsLink = true;
57
+ newInputConfig.value = rangeParentNode.href.replace("https://", "");
58
+ }
59
+ setInputConfig(newInputConfig);
60
+ setIsModalOpen(true);
61
+ setTextContent();
62
+ };
63
+
64
+ return (
65
+ <>
66
+ <button className={classNames("rich-text-tools-button ", node && "rich-text-tools-button-active")} title={t("tooltip_link")} onClick={addLinkTag}>
67
+ <FontAwesomeIcon icon={faLink} className="rich-text-tools-button-icon" />
68
+ </button>
69
+ <button
70
+ className={classNames("rich-text-tools-button")}
71
+ title={t("tooltip_remove_link")}
72
+ onClick={() => {
73
+ modifyText("unlink", false, null);
74
+ setTextContent();
75
+ }}
76
+ >
77
+ <FontAwesomeIcon icon={faUnlink} className="rich-text-tools-button-icon" />
78
+ </button>
79
+ <Modal
80
+ title={t("add_link_modal_title")}
81
+ open={isModalOpen}
82
+ zIndex={1100}
83
+ onOk={addLink}
84
+ onCancel={closeModal}
85
+ okText={t("confirm")}
86
+ cancelText={t("cancel")}
87
+ wrapClassName="ee-modal-dark"
88
+ >
89
+ <Input
90
+ addonBefore="https://"
91
+ value={inputConfig.value.replace("https://", "")}
92
+ onChange={(event) => setInputConfig({ ...inputConfig, value: "https://" + event.target.value })}
93
+ />
94
+ </Modal>
95
+ </>
96
+ );
97
+ };
98
+
99
+ export default Link;