近期抽空写了了一个可拖拽生成html文件的项目,项目截图:
利用了html5拖拽的属性,拖拽的流程分为:
1.可拖拽元素:设置拖拽属性draggable=“true”;
2.可释放元素:设置ondrop事件,设置dragover事件,dragover的默认行为是拒绝接受任何被拖放的元素。因此,我们必须阻止浏览器这种默认行为。e.preventDefault();
例子:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>demo 演示</title> <link rel="stylesheet" href="./a.css"></head><style> body { background: black; display: flex; } * { box-sizing: border-box; } #page { width: 375px; height: 750px; overflow: auto; margin: 30px; background: #ddd; padding: 20px; } #page div { min-height: 10px; outline: 1px solid red; padding: 20px; resize: both; overflow: auto; } #page span { outline: 1px solid blue; padding-left: 10px; padding-right: 10px; } #drag { background: #aaa; flex: 1; } .tag { width: 120px; border: 1px solid white; padding: 0; } .tag .li { list-style-type: none; margin: 10px; background: blue; color: white; text-align: center; padding: 10px; border-radius: 4px; cursor: pointer; display: block; } </style><body> <div id="page" ondragover="dragover(event)" ondrop='drop(event)'> </div> <div id="drag"> <div class="tag"> <div class='li' contenteditable="false" draggable="true" ondragstart="dragstart(event)" id='id1'> div </div> <span class='li' contenteditable="false" draggable="true" ondragstart="dragstart(event)" id='id2' > span </span> </div> </div></body></html><script> function dragover(e){ e.preventDefault(); } function dragstart(e){ e.dataTransfer.setData('msg',e.target.id) }function drop(e){ e.preventDefault(); e.stopPropagation(); var id =e.dataTransfer.getData('msg') var node = document.getElementById(id).cloneNode(true); node.id +='1' node.draggable=false; node.ondrop = drop; node.ondragover = dragover; node.setAttribute('contenteditable','true') node.innerText='' e.target.appendChild(node) }</script>
开发环境
开发环境选用了
create-react-app typescript。
1.安装react:运行 npx create-react-app projectName。
2.打开项目文件,运行npm i react @types/react @types/react-dom安装react。运行npm run eject生成webpack配置文件,注销这行// .filter(ext => useTypeScript ||!ext.includes('ts'))
主界面设计
1.左边:html绘制区域
2.右边功能区域,分为主功能区、元素列表区、html元素属性设置区功能设计与实现
元素模型设计
每个元素本身不存储状态,只需接受处理父组件传参属性(样式、属性、方法等),需定义专属样式、专属属性、元素类型,比如基础元素div:
interface Type{
css:any;
attr:any;
[props:string]:any
}
export class Div extends Component<Type>{
render(){
const {css,attr}:any = {...(this.props)}
return(
<div nDragOver={e=>{e.preventDefault();e.stopPropagation()}}
onDrop={e=>{e.stopPropagation();this.props.drop(this.props.id);}}
className='use-outline'
style={css}
onClick={this.props.onClick}
>
{attr.text !==""?attr.text:null}
{this.props.children}
</div>
)
}
static label='div'
//默认样式
static css:any = {
"text":"",
"width":'100%',
"minHeight":'20px'
}
//属性
static attr:any={
"text":""
}}
元素属性面板设计
设置元素的属性面板,属性分为所有元素的基础css属性,和父组件传递的属性(样式属性和专属功能属性),属性修改时通知父组件修改状态,代码如下:
import './cssMode.css';
import { mode } from "./interface";
import { Component } from 'react';
//所有元素的基础样式
const d_mode:mode={
'borderLeftWidth':'0px',
'borderRightWidth':'0px',
'borderTopWidth':'0px',
'borderBottomWidth':'0px',
'borderStyle':'none',
'borderColor':'none',
'paddingLeft':'0px',
'paddingRight':'0px',
'paddingTop':'0px',
'paddingBottom':'0px',
'marginLeft':'0px',
'marginRight':'0px',
'marginTop':'0px',
'marginBottom':'0px'
}
export class CssMode extends Component{
state:{css:any,attr:any,[prop:string]:any};
props:any;
constructor(props:any){
super(props);
this.props = props;
let css= Object.assign({},props.css,d_mode,props.css);
let attr = Object.assign({},props.attr)
this.state={
css,
attr
};
this.changeAttr = this.changeAttr.bind(this);
this.changeCss = this.changeCss.bind(this); }
render(){
const {css,attr} = this.state
return (
<>
<button onClick={this.props.delete}>删除</button>
{
Object.keys(attr).map((item:string,index:number)=>
<div className='css-mode' key={index}>
<label>{item}</label>
<input type="text" value={attr[item]} onChange={(e)=>this.changeAttr(e,item)} />
</div>)
}
<div style={{"borderBottom":"1px solid grey","margin":"20px 0"}}></div>
{
Object.keys(css).map((item:string,index:number)=>
<div className='css-mode' key={index}>
<label>{item}</label>
<input type="text" value={css[item]} onChange={(e)=>this.changeCss(e,item)} />
</div>
)
}
</>
)}
//修改样式
changeCss(e:any,item:string){
const css = {...(this.state.css)}
css[item]=e.target.value
this.setState({css},()=>{
this.props.changeCss && this.props.changeCss(this.state.css);
})
}
//修改属性
changeAttr(e:any,item:string){
const attr = {...(this.state.attr)}
attr[item]=e.target.value
this.setState({attr},()=>{
this.props.changeAttr && this.props.changeAttr(this.state.attr);
})
}
}
3. 元素列表注册与展示:在定义元素模板时设置了每个元素的标签、样式、专属属性这几个字段,根据这几个进行元素列表的注册与展示:
元素列表注册:定义了元素标签label、展示组件component、属性组件cssModey已经样式和属性
import * as El from './baseEl'
import { CssMode } from './cssMode';
let Ell:any={...El}const labels:any=[];
Object.keys(Ell).map((item:string)=>{
const el = Ell[item];
labels.push({
label:el.label,
type:el.label,
component:el,
cssMode:CssMode,
css:el.css,
attr:el.attr
})
})
元素列表展示:
{
list.map((li:any,index:number)=>
<button
key={index}
draggable='true'
onDragStart={()=>this.drag_start(li.type)}
>
{li.label}
</button>)
}
拖拽生成展示元素与展示元素状态管理
拖拽列表元素进可放入区域时会生成展示元素的id,并将拖拽元素的注册信息(元素标签、展示组件、属性面板组件、样式和属性)存入展示元素状态,页面根据展示元素的状态展示元素:
存入展示元素状态
drop(e:any){
e.preventDefault();
const {list,el} = this.state;
let node = {...(list.filter((item:any)=>item.type === this.targetType)[0])};
node.id = `el_${el.length}`;
node.children=[]
el.push(node);
this.setState({ el })
}
页面展示
list.map((item:any,key:number)=>
<item.component
key={key}
css={item.css}
attr={item.attr}
id={item.id}
onClick={(e:any)=>
{this.chooseEL(e,item)}}
>
</item.component>
)
5. 点击展示元素时,聚焦该展示组件,将该展示组件的属性传递给展示组件,调起该展示元素的属性展示面板,修改属性后,更新改展示元素的属性和样式,点击展示元素外区域取消属性面板的展示
聚焦展示元素并调起属性面板
async chooseEL(e:any,item:any){
e.stopPropagation();
await this.dropEl();
//获取元素组件根节点
const getNode=(el:any)=>{
if(el.classList.contains('use-outline')){
return el
}else{return el.parentNode}
}
let target = getNode(e.target);
target.classList.add('el-focus');
this.setState({
target:item
})
}
属性面板组件与展示:样式修改时调用changeCss修改展示元素状态,属性修改时调用changAttr修改展示组件属性
{
target?
<target.cssMode
css={target.css}
attr={target.attr}
changeCss={(data:any)=>{this.setCss(data,target,'css')}}
changeAttr={(data:any)=>{this.setCss(data,target,'attr')}}
delete={()=>{this.delete(target)}}
/>:null
}
6.目标展示元素被拖入新元素时,将子元素状态存入目标元素的children(子元素),并更新展示
目标元素拖入子元素
elDrop(id:any,item:any){
const {list,el} = this.state;
const node = {...(list.filter((item:any)=>item.type === this.targetType)[0])};
node.id = `${id}_${item.children.length}`;
node.children=[];
item.children.push(node);
this.setState({ el })
}
修改目标元素展示用来展示子元素
render(){
let {list,el,target} = this.state;
const deepEl:any = (list:any)=>{
return list.map((item:any,key:number)=>
item.delete?null:
<item.component
key={key}
css={item.css}
attr={item.attr}
id={item.id}
drop={(id:any)=>{this.elDrop(id,item)}}
onClick={(e:any)=>{this.chooseEL(e,item)}}
>
{deepEl(item.children)}
</item.component>)
}
return (
<div className="App" >
<div id="page" onDragOver={e=>e.preventDefault()} onDrop={this.drop} onClick={this.dropEl}>
{
deepEl(el)
}
</div>
其它功能:加展示元素outline用于调试、删除展示元素等
实现编译、导出html
上述功能实现后,点击生成html文件,发送ajax请求,将展示元素的状态传给node.js处理,
1. 安装express服务
npm i express -S
2. 起服务,express收到接口请求后开始编译
const express = require('express');
const app = new express();
app.use(express.static(path.resolve(__dirname,'../static')));
app.all('*', (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
//生成html文件接口请求
app.get('/build_file', (req, res,next) => {
parseData.init(JSON.parse(req.query.msg))
res.send({code:0});
});
app.listen('9090', () => {
console.log(`Server running at http://localhost:9090/`);
});
3.处理展示元素状态生成css文件,根据转递过来的展示元素id生成样式class类名,展示元素的css属性生成css属性
//构建css
build_css(list) {
if(list.length == 0) return ''
//css module大写转横杠
const transStyle = str => {
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}
let cssMsg = fs.readFileSync(path.resolve(__dirname,'../src/modern/base.css'),'utf-8')
list.map(item => {
cssMsg += '.' + item.id + '{';
let css = item.css;
Object.keys(css).map(li => {
cssMsg += transStyle(li) + ':' + css[li] + ';'
})
cssMsg += '} '
cssMsg += this.build_css(item.children)
})
return cssMsg
}
4.处理展示元素html标签与属性,根据每个元素模板的构成编译生成html文件
class HtmlBuild {
static init(list){
let msg = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>drag html</title> <link rel="stylesheet" href="./index.css"></head><body>`
list.map(li => {
msg += HtmlBuild[li.type](li)
})
return msg +`</body></html>`;
}
static div(item) {
let msg = `<div class="${item.id}">${item.attr.text}`
item.children.map(li => {
msg += HtmlBuild[li.type](li)
})
msg += `</div>` return msg + '\n';
}
static span(item) {
return `<span class="${item.id}">${item.attr.text}</span>\n`
}
static img(item) {
return `<img class="${item.id}" src="${item.attr.src}" />\n`
}
static listItem(item) {
return`<div class="list-item ${item.id}">
<img src="${item.attr.img}" alt="" />
<h6>${item.attr.title}</h6>
<p>${item.attr.desc}</p>
</div>\n`
}
static detail(item) {
let msg = `<div class="detail-item ${item.id} ">
<img src=${item.attr.img} alt="" />
<p class='name'>${item.attr.name}</p>
<div class='price'><span>¥</span>${item.attr.price}</div>
<div class = 'mid'>
<span>产品详情</span>
<div class="line"></div>
</div>
<div class="detail">`
item.children.map(li => {
msg += HtmlBuild[li.type](li)
})
msg+=`</div></div>`
return msg + '\n';
}
}
/dists文件夹下可看到生成的文件,index.html, 运行npm run build 可将编译功能界面打包,下次直接点开打包文件就可以了
github地址:github.com/nicole11223…