@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.
- package/README.md +63 -0
- package/build/asset-manifest.json +12 -0
- package/build/favicon.ico +0 -0
- package/build/index.html +1 -0
- package/build/logo192.png +0 -0
- package/build/logo512.png +0 -0
- package/build/manifest.json +25 -0
- package/build/robots.txt +3 -0
- package/build/static/css/main.588cb535.css +9 -0
- package/build/static/js/206.a4343501.chunk.js +1 -0
- package/build/static/js/main.053d366a.js +2 -0
- package/build/static/js/main.053d366a.js.LICENSE.txt +56 -0
- package/package.json +64 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +50 -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/App.js +15 -0
- package/src/components/EmailEditor/assets/App.css +339 -0
- package/src/components/EmailEditor/assets/Columns.css +309 -0
- package/src/components/EmailEditor/assets/Header.css +174 -0
- package/src/components/EmailEditor/assets/ImageBlock.css +12 -0
- package/src/components/EmailEditor/assets/Preview.css +30 -0
- package/src/components/EmailEditor/assets/RichText.css +199 -0
- package/src/components/EmailEditor/assets/RightSettings.css +520 -0
- package/src/components/EmailEditor/assets/Sidebar.css +195 -0
- package/src/components/EmailEditor/components/BlockItems/ButtonBlock.js +25 -0
- package/src/components/EmailEditor/components/BlockItems/DividerBlock.js +19 -0
- package/src/components/EmailEditor/components/BlockItems/HeadingBlock.js +16 -0
- package/src/components/EmailEditor/components/BlockItems/ImageBlock.js +28 -0
- package/src/components/EmailEditor/components/BlockItems/MenuBlock.js +52 -0
- package/src/components/EmailEditor/components/BlockItems/SocialLinkBlocks.js +26 -0
- package/src/components/EmailEditor/components/BlockItems/SpacerBlock.js +23 -0
- package/src/components/EmailEditor/components/BlockItems/TextBlock.js +16 -0
- package/src/components/EmailEditor/components/BlockItems/index.js +25 -0
- package/src/components/EmailEditor/components/ColorPicker/index.js +26 -0
- package/src/components/EmailEditor/components/Column/index.js +253 -0
- package/src/components/EmailEditor/components/Header/index.js +243 -0
- package/src/components/EmailEditor/components/LeftSideBar/index.js +253 -0
- package/src/components/EmailEditor/components/Main/index.js +281 -0
- package/src/components/EmailEditor/components/Preview/index.js +97 -0
- package/src/components/EmailEditor/components/RichText/Bold.js +37 -0
- package/src/components/EmailEditor/components/RichText/FontColor.js +39 -0
- package/src/components/EmailEditor/components/RichText/InsertOrderedList.js +36 -0
- package/src/components/EmailEditor/components/RichText/InsertUnorderedList.js +36 -0
- package/src/components/EmailEditor/components/RichText/Italic.js +36 -0
- package/src/components/EmailEditor/components/RichText/Link.js +99 -0
- package/src/components/EmailEditor/components/RichText/RichTextLayout.js +53 -0
- package/src/components/EmailEditor/components/RichText/Strikethrough.js +36 -0
- package/src/components/EmailEditor/components/RichText/TextAlign.js +58 -0
- package/src/components/EmailEditor/components/RichText/Underline.js +36 -0
- package/src/components/EmailEditor/components/RichText/index.js +210 -0
- package/src/components/EmailEditor/components/RightSetting/index.js +126 -0
- package/src/components/EmailEditor/components/StyleSettings/ButtonStyleSettings.js +141 -0
- package/src/components/EmailEditor/components/StyleSettings/ColumnStyleSettings.js +241 -0
- package/src/components/EmailEditor/components/StyleSettings/DividerStyleSettings.js +111 -0
- package/src/components/EmailEditor/components/StyleSettings/HeadingStyleSettings.js +162 -0
- package/src/components/EmailEditor/components/StyleSettings/ImageStyleSettings.js +217 -0
- package/src/components/EmailEditor/components/StyleSettings/MenuStyleSettings.js +177 -0
- package/src/components/EmailEditor/components/StyleSettings/PaddingSettings.js +30 -0
- package/src/components/EmailEditor/components/StyleSettings/SocialLinkSettings.js +250 -0
- package/src/components/EmailEditor/components/StyleSettings/SpacerStyleSettings.js +38 -0
- package/src/components/EmailEditor/components/StyleSettings/TextStyleSettings.js +108 -0
- package/src/components/EmailEditor/components/StyleSettings/index.js +32 -0
- package/src/components/EmailEditor/configs/getBlockConfigsList.js +263 -0
- package/src/components/EmailEditor/configs/getColumnConfigFunc.js +59 -0
- package/src/components/EmailEditor/configs/getColumnsSettings.js +246 -0
- package/src/components/EmailEditor/configs/useDataSource.js +19 -0
- package/src/components/EmailEditor/index.js +93 -0
- package/src/components/EmailEditor/reducers/index.js +173 -0
- package/src/components/EmailEditor/translation/en.js +166 -0
- package/src/components/EmailEditor/translation/index.js +39 -0
- package/src/components/EmailEditor/translation/zh.js +166 -0
- package/src/components/EmailEditor/utils/classNames.js +5 -0
- package/src/components/EmailEditor/utils/dataToHTML.js +323 -0
- package/src/components/EmailEditor/utils/exportValidation.js +296 -0
- package/src/components/EmailEditor/utils/helpers.js +48 -0
- package/src/components/EmailEditor/utils/pexels.js +20 -0
- package/src/components/EmailEditor/utils/useSection.js +24 -0
- package/src/components/EmailEditor/utils/useStyleLayout.js +82 -0
- package/src/index.css +99 -0
- package/src/index.js +15 -0
- package/src/logo.svg +1 -0
- package/src/pages/AppPage/index.js +10 -0
- package/src/pages/Dashboard/Header.js +192 -0
- package/src/pages/Dashboard/defaultBlockList.json +1758 -0
- package/src/pages/Dashboard/index.js +48 -0
- package/src/reportWebVitals.js +13 -0
- 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;
|