
⭐前言
大家好,我是yma16,本文分享 前端react——从零开始的拖拽生成email(第一章)
背景
作为2025练手项目
前端系列文章
vue3 + fastapi 实现选择目录所有文件自定义上传到服务器
前端vue2、vue3去掉url路由“ # ”号——nginx配置
csdn新星计划vue3+ts+antd赛道——利用inscode搭建vue3(ts)+antd前端模板
认识vite_vue3 初始化项目到打包
python_selenuim获取csdn新星赛道选手所在城市用echarts地图显示
让大模型分析csdn文章质量 —— 提取csdn博客评论在文心一言分析评论区内容
前端vue3——html2canvas给网站截图生成宣传海报
前端——html拖拽原理
前端 富文本编辑器原理——从javascript、html、css开始入门
前端老古董execCommand——操作 选中文本 样式
前端如何在30秒内实现吸管拾色器?
前端——原生Selection api操作选中文本 样式、取消样式(解决标签的无限嵌套问题)
⭐初始化react app
创建 drag-email 目录的react app
# npx create-react-app drag-email
暴露 webpack配置,方便后续引入less
# npm run eject
webpack.config 配置加载less
// 加载less
const lessRegex = /\.less$/i;
配置添加
{
test: lessRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction? shouldUseSourceMap: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'less-loader'
),
}
⭐封装拖拽
主要针对当前元素和目标元素添加drag和drop事件。
interface ConfigType {
source: any;
target: any;
}
class DragOption {
public sourceDom = null;
public targetDom = null;
public dragConfig = {
curentDrag: null,
};
constructor(config: { source: any }) {
const { source } = config;
this.sourceDom = source;
this.addListen();
}
// 初始化
public addListen() {
this.dragStart();
this.drag();
this.dragend();
}
public removeListen() {
this.removeDragStart();
this.removeDrag();
this.removeDragend();
}
/* 在可拖动的目标上触发的事件 */
public dragStart() {
// @ts-ignore
this.sourceDom.addEventListener("dragstart", (event: any) => {
// 保存被拖动元素的引用 dragging
this.dragConfig.curentDrag = event.target;
// 拖拽 样式 dragging
event.target.classList.add("dragging");
});
}
public removeDragStart() {
// @ts-ignore
this.sourceDom.removeEventListener("dragstart", (event: any) => {
// 保存被拖动元素的引用 dragging
this.dragConfig.curentDrag = event.target;
// 拖拽 样式 dragging
event.target.classList.add("dragging");
});
}
public drag() {
// @ts-ignore
this.sourceDom.addEventListener("drag", (event: any) => {
// 拖拽中
console.log("dragging");
});
}
public removeDrag() {
// @ts-ignore
this.sourceDom.removeEventListener("drag", (event: any) => {
// 拖拽中
console.log("dragging");
});
}
public dragend() {
// @ts-ignore
this.sourceDom.addEventListener("dragend", (event: any) => {
// 在可拖动元素进入潜在的放置目标时高亮显示该目标
if (event.target.classList.contains("dropzone")) {
event.target.classList.add("dragover");
}
});
}
public removeDragend() {
// @ts-ignore
this.sourceDom.removeEventListener("dragend", (event: any) => {
// 在可拖动元素进入潜在的放置目标时高亮显示该目标
if (event.target.classList.contains("dropzone")) {
event.target.classList.add("dragover");
}
});
}
}
class DropOption {
public targetDom = null;
public dragConfig = {
curentDrag: null,
};
constructor(config: { target: any }) {
const { target } = config;
this.targetDom = target;
this.addListen();
}
// 初始化
public addListen() {
this.dragOver();
this.dragenter();
this.dragleave();
this.drop();
}
// 去掉监听
public removeListen() {
this.removeDragOver();
this.removeDragenter();
this.removeDragleave();
this.removeDrop();
}
/* 在放置目标上触发的事件 */
public dragOver() {
// @ts-ignore
this.targetDom.addEventListener(
"dragover",
(event: any) => {
// 阻止默认行为以允许放置
event.preventDefault();
},
false
);
}
public removeDragOver() {
// @ts-ignore
this.targetDom.removeEventListener(
"dragover",
(event: any) => {
// 阻止默认行为以允许放置
event.preventDefault();
},
false
);
}
public dragenter() {
// @ts-ignore
this.targetDom.addEventListener("dragenter", (event: any) => {
// 拖动结束,去掉dragging
event.target.classList.remove("dragging");
});
}
public removeDragenter() {
// @ts-ignore
this.targetDom.removeEventListener("dragenter", (event: any) => {
// 拖动结束,去掉dragging
event.target.classList.remove("dragging");
});
}
public dragleave() {
// @ts-ignore
this.targetDom.addEventListener("dragleave", (event: any) => {
// 在可拖动元素离开潜在放置目标元素时重置该目标的背景
if (event.target.classList.contains("dropzone")) {
event.target.classList.remove("dragover");
}
});
}
public removeDragleave() {
// @ts-ignore
this.targetDom.removeEventListener("dragleave", (event: any) => {
// 在可拖动元素离开潜在放置目标元素时重置该目标的背景
if (event.target.classList.contains("dropzone")) {
event.target.classList.remove("dragover");
}
});
}
public dropEvent(event: any) {
// 阻止默认行为(会作为某些元素的链接打开)
event.preventDefault();
console.log("add before");
// 将被拖动元素移动到选定的目标元素中
if (event.target.classList.contains("dropzone")) {
event.target.classList.remove("dragover");
// 删除自身
// config.draged.parentNode.removeChild(config.draged);
// event.target.appendChild(config.draged);
// 创建虚拟dom 组件自定义的地方
const vDom = document.createElement("div");
console.log(vDom, "vDom");
vDom.classList.add("container-box-left-component");
event.target.appendChild(vDom);
console.log("add component");
}
}
public drop() {
console.log("add drop listen");
//@ts-ignore
this.targetDom.addEventListener("drop", this.dropEvent);
}
public removeDrop() {
console.log("removeEventListener drop listen");
//@ts-ignore
this.targetDom.removeEventListener("drop", this.dropEvent);
}
}
export { DragOption, DropOption };
⭐页面雏形
页面采用简单的左右布局
import "./App.css";
import { useEffect, useRef, useMemo } from "react";
import { DragOption, DropOption } from "./config/const";
function App() {
const sourceRef = useRef(null);
const targetRef = useRef(null);
const componentOptions = [
{
name: "文字",
type: "text",
content: "输入文字",
id: "1",
},
{
name: "图片",
type: "image",
content: "图片",
id: "2",
},
{
name: "列",
type: "column",
content: "",
id: "3",
},
{
name: "分割线",
type: "line",
content: "",
id: "4",
},
{
name: "按钮",
type: "button",
content: "按钮",
id: "5",
},
];
const onClear = () => {
if (targetRef.current) {
// @ts-ignore
targetRef.current.innerHTML = "";
}
};
useMemo(() => {
console.log("componentOptions", componentOptions);
}, [componentOptions]);
useEffect(() => {
let sourceClassArr: any = [];
if (sourceRef.current) {
componentOptions.map((item) => {
const source = document.getElementById(item.id);
const dragOption = {
source,
};
const DragClass = new DragOption(dragOption);
console.log(DragClass);
sourceClassArr.push(DragClass);
});
}
return () => {
sourceClassArr.forEach((item: any) => {
item.removeListen();
item = null;
});
console.log("卸载 sourceRef");
};
}, [sourceRef]);
useEffect(() => {
let dropClass: any = null;
if (targetRef.current) {
dropClass = new DropOption({
target: targetRef.current,
});
console.log("dropClass");
}
return () => {
dropClass?.removeListen();
dropClass = null;
console.log("卸载 targetRef");
};
}, [targetRef]);
// @ts-ignore
return (
<div className="container">
<div className="container-header">
<button className="base-button">导出 邮件html</button>
<button className="base-button" onClick={onClear}>
清空
</button>
</div>
<div className="container-box">
<div className="container-box-left" ref={sourceRef}>
{componentOptions.map((item) => {
return (
<div
className="container-box-left-component basic-component"
draggable="true"
id={item.id}
key={item.id}
>
{item.name}
</div>
);
})}
</div>
<div className="container-box-right">
<div
className="container-box-right-box dropzone"
id="droptarget"
ref={targetRef}
>
{/* 可以拖到这里 */}
</div>
</div>
</div>
</div>
);
}
export default App;
页面基本布局
⭐下一步计划
- 引入ui框架美化样式
- 组件可编辑(文字输入、图片上传、组件padding设置)
- 富文本编辑选中文本的样式放到下周实现 2.15 号左右
⭐结束
本文分享到这结束,如有错误或者不足之处欢迎指出!
👍 点赞,是我创作的动力!
⭐️ 收藏,是我努力的方向!
✏️ 评论,是我进步的财富!
💖 感谢你的阅读!