可视化拖拽组件库一些技术要点原理分析
本文已获得原作者的独家授权,有想转载的朋友们可以在后台联系我申请开白哦!
PS:欢迎掘友们向我投稿哦,被采用的文章还可以送你掘金精美周边!
本文主要对以下技术要点进行分析:
编辑器
自定义组件
拖拽
删除组件、调整图层层级
放大缩小
撤消、重做
组件属性设置
吸附
预览、保存代码
绑定事件
绑定动画
导入 PSD
手机模式
为了让本文更加容易理解,我将以上技术要点结合在一起写了一个可视化拖拽组件库 DEMO:
github 项目地址
在线预览
建议结合源码一起阅读,效果更好(这个 DEMO 使用的是 Vue 技术栈)。
1.编辑器
先来看一下页面的整体结构。
这一节要讲的编辑器其实就是中间的画布。它的作用是:当从左边组件列表拖拽出一个组件放到画布中时,画布要把这个组件渲染出来。
这个编辑器的实现思路是:
用一个数组
componentData
维护编辑器中的数据。把组件拖拽到画布中时,使用
push()
方法将新的组件数据添加到componentData
。编辑器使用
v-for
指令遍历componentData
,将每个组件逐个渲染到画布(也可以使用 JSX 语法结合render()
方法代替)。
编辑器渲染的核心代码如下所示:
<component v-for="item in componentData":key="item.id":is="item.component":style="item.style":propValue="item.propValue"
/>
每个组件数据大概是这样:
{component: 'v-text', label: '文字', propValue: '文字', icon: 'el-icon-edit', animations: [], events: {}, style: { width: 200,height: 33,fontSize: 14,fontWeight: 500,lineHeight: '',letterSpacing: 0,textAlign: '',color: '',},
}
在遍历 componentData
组件数据时,主要靠 is
属性来识别出真正要渲染的是哪个组件。
例如要渲染的组件数据是 { component: 'v-text' }
,则 <component :is="item.component" />
会被转换为 <v-text />
。当然,你这个组件也要提前注册到 Vue 中。
如果你想了解更多 is
属性的资料,请查看官方文档。
2. 自定义组件
原则上使用第三方组件也是可以的,但建议你最好封装一下。不管是第三方组件还是自定义组件,每个组件所需的属性可能都不一样,所以每个组件数据可以暴露出一个属性 propValue
用于传递值。
例如 a 组件只需要一个属性,你的 propValue
可以这样写:propValue: 'aaa'
。如果需要多个属性,propValue
则可以是一个对象:
propValue: {a: 1,b: 'text'
}
在这个 DEMO 组件库中我定义了三个组件。
图片组件 Picture
:
<template><div><img :src="propValue"></div>
</template><script>
export default {props: {propValue: {type: String,require: true,},},
}
</script>
按钮组件 VButton
:
<template><button>{{ propValue }}</button>
</template><script>
export default {props: {propValue: {type: String,default: '',},},
}
</script>
文本组件 VText
:
<template><textarea v-if="editMode == 'edit'":value="propValue"@input="handleInput"ref="v-text"></textarea><div v-else><div v-for="(text, index) in propValue.split('\n')" :key="index">{{ text }}</div></div>
</template><script>
import { mapState } from 'vuex'export default {props: {propValue: {type: String,},element: {type: Object,},},computed: mapState(['editMode',]),methods: {handleInput(e) {this.$emit('input', this.element, e.target.value)},},
}
</script>
3. 拖拽
从组件列表到画布
一个元素如果要设为可拖拽,必须给它添加一个 draggable
属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:
dragstart
事件,在拖拽刚开始时触发。它主要用于将拖拽的组件信息传递给画布。drop
事件,在拖拽结束时触发。主要用于接收拖拽的组件信息。
先来看一下左侧组件列表的代码:
<div @dragstart="handleDragStart"><div v-for="(item, index) in componentList" :key="index" draggable :data-index="index"><i :class="item.icon"></i><span>{{ item.label }}</span></div>
</div>
handleDragStart(e) {e.dataTransfer.setData('index', e.target.dataset.index)
}
可以看到给列表中的每一个组件都设置了 draggable
属性。另外,在触发 dragstart
事件时,使用 dataTransfer.setData()
传输数据。再来看一下接收数据的代码:
<div @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent"><Editor />
</div>
handleDrop(e) {e.preventDefault()e.stopPropagation()const component = deepCopy(componentList[e.dataTransfer.getData('index')])this.$store.commit('addComponent', component)
}
触发 drop
事件时,使用 dataTransfer.getData()
接收传输过来的索引数据,然后根据索引找到对应的组件数据,再添加到画布,从而渲染组件。
组件在画布中移动
首先需要将画布设为相对定位 position: relative
,然后将每个组件设为绝对定位 position: absolute
。除了这一点外,还要通过监听三个事件来进行移动:
mousedown
事件,在组件上按下鼠标时,记录组件当前的位置,即 xy 坐标(为了方便讲解,这里使用的坐标轴,实际上 xy 对应的是 css 中的left
和top
。mousemove
事件,每次鼠标移动时,都用当前最新的 xy 坐标减去最开始的 xy 坐标,从而计算出移动距离,再改变组件位置。mouseup
事件,鼠标抬起时结束移动。
handleMouseDown(e) {e.stopPropagation()this.$store.commit('setCurComponent', { component: this.element, zIndex: this.zIndex })const pos = { ...this.defaultStyle }const startY = e.clientYconst startX = e.clientXconst startTop = Number(pos.top)const startLeft = Number(pos.left)const move = (moveEvent) => {const currX = moveEvent.clientXconst currY = moveEvent.clientYpos.top = currY - startY + startToppos.left = currX - startX + startLeftthis.$store.commit('setShapeStyle', pos)}const up = () => {document.removeEventListener('mousemove', move)document.removeEventListener('mouseup', up)}document.addEventListener('mousemove', move)document.addEventListener('mouseup', up)
}
4. 删除组件、调整图层层级
改变图层层级
由于拖拽组件到画布中是有先后顺序的,所以可以按照数据顺序来分配图层层级。
例如画布新增了五个组件 abcde,那它们在画布数据中的顺序为 [a, b, c, d, e]
,图层层级和索引一一对应,即它们的 z-index
属性值是 01234(后来居上)。用代码表示如下:
<div v-for="(item, index) in componentData" :zIndex="index"></div>
如果不了解 z-index
属性的,请看一下 MDN 文档。
理解了这一点之后,改变图层层级就很容易做到了。改变图层层级,即是改变组件数据在 componentData
数组中的顺序。例如有 [a, b, c]
三个组件,它们的图层层级从低到高顺序为 abc(索引越大,层级越高)。
如果要将 b 组件上移,只需将它和 c 调换顺序即可:
const temp = componentData[1]
componentData[1] = componentData[2]
componentData[2] = temp
同理,置顶置底也是一样,例如我要将 a 组件置顶,只需将 a 和最后一个组件调换顺序即可:
const temp = componentData[0]
componentData[0] = componentData[componentData.lenght - 1]
componentData[componentData.lenght - 1] = temp
删除组件
删除组件非常简单,一行代码搞定:componentData.splice(index, 1)
。
5. 放大缩小
细心的网友可能会发现,点击画布上的组件时,组件上会出现 8 个小圆点。这 8 个小圆点就是用来放大缩小用的。实现原理如下:
1. 在每个组件外面包一层 Shape
组件,Shape
组件里包含 8 个小圆点和一个 <slot>
插槽,用于放置组件。
<Shape v-for="(item, index) in componentData":defaultStyle="item.style":style="getShapeStyle(item.style, index)":key="item.id":active="item === curComponent":element="item":zIndex="index"
><component:is="item.component":style="getComponentStyle(item.style)":propValue="item.propValue"/>
</Shape>
Shape
组件内部结构:
<template><div :class="{ active: this.active }" @click="selectCurComponent" @mousedown="handleMouseDown"@contextmenu="handleContextMenu"><divv-for="(item, index) in (active? pointList : [])"@mousedown="handleMouseDownOnPoint(item)":key="index":style="getPointStyle(item)"></div><slot></slot></div>
</template>
2. 点击组件时,将 8 个小圆点显示出来。
起作用的是这行代码 :active="item === curComponent"
。
3. 计算每个小圆点的位置。
先来看一下计算小圆点位置的代码:
const pointList = ['t', 'r', 'b', 'l', 'lt', 'rt', 'lb', 'rb']getPointStyle(point) {const { width, height } = this.defaultStyleconst hasT = /t/.test(point)const hasB = /b/.test(point)const hasL = /l/.test(point)const hasR = /r/.test(point)let newLeft = 0let newTop = 0if (point.length === 2) {newLeft = hasL? 0 : widthnewTop = hasT? 0 : height} else {if (hasT || hasB) {newLeft = width / 2newTop = hasT? 0 : height}if (hasL || hasR) {newLeft = hasL? 0 : widthnewTop = Math.floor(height / 2)}}const style = {marginLeft: hasR? '-4px' : '-3px',marginTop: '-3px',left: `${newLeft}px`,top: `${newTop}px`,cursor: point.split('').reverse().map(m => this.directionKey[m]).join('') + '-resize',}return style
}
计算小圆点的位置需要获取一些信息:
组件的高度
height
、宽度width
注意,小圆点也是绝对定位的,相对于 Shape
组件。所以有四个小圆点的位置很好确定:
左上角的小圆点,坐标
left: 0, top: 0
右上角的小圆点,坐标
left: width, top: 0
左下角的小圆点,坐标
left: 0, top: height
右下角的小圆点,坐标
left: width, top: height
另外的四个小圆点需要通过计算间接算出来。例如左边中间的小圆点,计算公式为 left: 0, top: height / 2
,其他小圆点同理。
4. 点击小圆点时,可以进行放大缩小操作。
handleMouseDownOnPoint(point) {const downEvent = window.eventdownEvent.stopPropagation()downEvent.preventDefault()const pos = { ...this.defaultStyle }const height = Number(pos.height)const width = Number(pos.width)const top = Number(pos.top)const left = Number(pos.left)const startX = downEvent.clientXconst startY = downEvent.clientYlet needSave = falseconst move = (moveEvent) => {needSave = trueconst currX = moveEvent.clientXconst currY = moveEvent.clientYconst disY = currY - startYconst disX = currX - startXconst hasT = /t/.test(point)const hasB = /b/.test(point)const hasL = /l/.test(point)const hasR = /r/.test(point)const newHeight = height + (hasT? -disY : hasB? disY : 0)const newWidth = width + (hasL? -disX : hasR? disX : 0)pos.height = newHeight > 0? newHeight : 0pos.width = newWidth > 0? newWidth : 0pos.left = left + (hasL? disX : 0)pos.top = top + (hasT? disY : 0)this.$store.commit('setShapeStyle', pos)}const up = () => {document.removeEventListener('mousemove', move)document.removeEventListener('mouseup', up)needSave && this.$store.commit('recordSnapshot')}document.addEventListener('mousemove', move)document.addEventListener('mouseup', up)
}
它的原理是这样的:
点击小圆点时,记录点击的坐标 xy。
假设我们现在向下拖动,那么 y 坐标就会增大。
用新的 y 坐标减去原来的 y 坐标,就可以知道在纵轴方向的移动距离是多少。
最后再将移动距离加上原来组件的高度,就可以得出新的组件高度。
如果是正数,说明是往下拉,组件的高度在增加。如果是负数,说明是往上拉,组件的高度在减少。
6. 撤消、重做
撤销重做的实现原理其实挺简单的,先看一下代码:
snapshotData: [],
snapshotIndex: -1, undo(state) {if (state.snapshotIndex >= 0) {state.snapshotIndex--store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))}
},redo(state) {if (state.snapshotIndex < state.snapshotData.length - 1) {state.snapshotIndex++store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))}
},setComponentData(state, componentData = []) {Vue.set(state, 'componentData', componentData)
},recordSnapshot(state) {state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)if (state.snapshotIndex < state.snapshotData.length - 1) {state.snapshotData = state.snapshotData.slice(0, state.snapshotIndex + 1)}
},
用一个数组来保存编辑器的快照数据。保存快照就是不停地执行 push()
操作,将当前的编辑器数据推入 snapshotData
数组,并增加快照索引 snapshotIndex
。目前以下几个动作会触发保存快照操作:
新增组件
删除组件
改变图层层级
拖动组件结束时
...
撤销
假设现在 snapshotData
保存了 4 个快照。即 [a, b, c, d]
,对应的快照索引为 3。如果这时进行了撤销操作,我们需要将快照索引减 1,然后将对应的快照数据赋值给画布。
例如当前画布数据是 d,进行撤销后,索引 -1,现在画布的数据是 c。
重做
明白了撤销,那重做就很好理解了,就是将快照索引加 1,然后将对应的快照数据赋值给画布。
不过还有一点要注意,就是在撤销操作中进行了新的操作,要怎么办呢?有两种解决方案:
新操作替换当前快照索引后面所有的数据。还是用刚才的数据
[a, b, c, d]
举例,假设现在进行了两次撤销操作,快照索引变为 1,对应的快照数据为 b,如果这时进行了新的操作,对应的快照数据为 e。那 e 会把 cd 顶掉,现在的快照数据为[a, b, e]
。不顶掉数据,在原来的快照中新增一条记录。用刚才的例子举例,e 不会把 cd 顶掉,而是在 cd 之前插入,即快照数据变为
[a, b, e, c, d]
。
我采用的是第一种方案。
7. 吸附
什么是吸附?就是在拖拽组件时,如果它和另一个组件的距离比较接近,就会自动吸附在一起。
吸附的代码大概在 300 行左右,建议自己打开源码文件看(文件路径:src\components\Editor\MarkLine.vue
)。这里不贴代码了,主要说说原理是怎么实现的。
标线
在页面上创建 6 条线,分别是三横三竖。这 6 条线的作用是对齐,它们什么时候会出现呢?
上下方向的两个组件左边、中间、右边对齐时会出现竖线
左右方向的两个组件上边、中间、下边对齐时会出现横线
具体的计算公式主要是根据每个组件的 xy 坐标和宽度高度进行计算的。例如要判断 ab 两个组件的左边是否对齐,则要知道它们每个组件的 x 坐标;如果要知道它们右边是否对齐,除了要知道 x 坐标,还要知道它们各自的宽度。
a.x == b.xa.x + a.width == b.x + b.width
在对齐的时候,显示标线。
另外还要判断 ab 两个组件是否 “足够” 近。如果足够近,就吸附在一起。是否足够近要靠一个变量来判断:
diff: 3,
小于等于 diff
像素则自动吸附。
吸附
吸附效果是怎么实现的呢?
假设现在有 ab 组件,a 组件坐标 xy 都是 0,宽高都是 100。现在假设 a 组件不动,我们正在拖拽 b 组件。当把 b 组件拖到坐标为 x: 0, y: 103
时,由于 103 - 100 <= 3(diff)
,所以可以判定它们已经接近得足够近。这时需要手动将 b 组件的 y 坐标值设为 100,这样就将 ab 组件吸附在一起了。
优化
在拖拽时如果 6 条标线都显示出来会不太美观。所以我们可以做一下优化,在纵横方向上最多只同时显示一条线。实现原理如下:
a 组件在左边不动,我们拖着 b 组件往 a 组件靠近。
这时它们最先对齐的是 a 的右边和 b 的左边,所以只需要一条线就够了。
如果 ab 组件已经靠近,并且 b 组件继续往左边移动,这时就要判断它们俩的中间是否对齐。
b 组件继续拖动,这时需要判断 a 组件的左边和 b 组件的右边是否对齐,也是只需要一条线。
可以发现,关键的地方是我们要知道两个组件的方向。即 ab 两个组件靠近,我们要知道到底 b 是在 a 的左边还是右边。
这一点可以通过鼠标移动事件来判断,之前在讲解拖拽的时候说过,mousedown
事件触发时会记录起点坐标。所以每次触发 mousemove
事件时,用当前坐标减去原来的坐标,就可以判断组件方向。例如 x 方向上,如果 b.x - a.x
的差值为正,说明是 b 在 a 右边,否则为左边。
eventBus.$emit('move', this.$el, currY - startY > 0, currX - startX > 0)
8. 组件属性设置
每个组件都有一些通用属性和独有的属性,我们需要提供一个能显示和修改属性的地方。
{component: 'v-text', label: '文字', propValue: '文字', icon: 'el-icon-edit', animations: [], events: {}, style: { width: 200,height: 33,fontSize: 14,fontWeight: 500,lineHeight: '',letterSpacing: 0,textAlign: '',color: '',},
}
我定义了一个 AttrList
组件,用于显示每个组件的属性。
<template><div><el-form><el-form-item v-for="(key, index) in styleKeys" :key="index" :label="map[key]"><el-color-picker v-if="key == 'borderColor'" v-model="curComponent.style[key]"></el-color-picker><el-color-picker v-else-if="key == 'color'" v-model="curComponent.style[key]"></el-color-picker><el-color-picker v-else-if="key == 'backgroundColor'" v-model="curComponent.style[key]"></el-color-picker><el-select v-else-if="key == 'textAlign'" v-model="curComponent.style[key]"><el-optionv-for="item in options":key="item.value":label="item.label":value="item.value"></el-option></el-select><el-input type="number" v-else v-model="curComponent.style[key]" /></el-form-item><el-form-item label="内容" v-if="curComponent && curComponent.propValue && !excludes.includes(curComponent.component)"><el-input type="textarea" v-model="curComponent.propValue" /></el-form-item></el-form></div>
</template>
代码逻辑很简单,就是遍历组件的 style
对象,将每一个属性遍历出来。并且需要根据具体的属性用不同的组件显示出来,例如颜色属性,需要用颜色选择器显示;数值类的属性需要用 type=number
的 input 组件显示等等。
为了方便用户修改属性值,我使用 v-model
将组件和值绑定在一起。
9. 预览、保存代码
预览和编辑的渲染原理是一样的,区别是不需要编辑功能。所以只需要将原先渲染组件的代码稍微改一下就可以了。
<Shape v-for="(item, index) in componentData":defaultStyle="item.style":style="getShapeStyle(item.style, index)":key="item.id":active="item === curComponent":element="item":zIndex="index"
><component:is="item.component":style="getComponentStyle(item.style)":propValue="item.propValue"/>
</Shape>
经过刚才的介绍,我们知道 Shape
组件具备了拖拽、放大缩小的功能。现在只需要将 Shape
组件去掉,外面改成套一个普通的 DIV 就可以了(其实不用这个 DIV 也行,但为了绑定事件这个功能,所以需要加上)。
<div v-for="(item, index) in componentData" :key="item.id"><component:is="item.component":style="getComponentStyle(item.style)":propValue="item.propValue"/>
</div>
保存代码的功能也特别简单,只需要保存画布上的数据 componentData
即可。保存有两种选择:
保存到服务器
本地保存
在 DEMO 上我使用的 localStorage
保存在本地。
10. 绑定事件
每个组件有一个 events
对象,用于存储绑定的事件。目前我只定义了两个事件:
alert 事件
redirect 事件
const events = {redirect(url) {if (url) {window.location.href = url}},alert(msg) {if (msg) {alert(msg)}},
}const mixins = {methods: events,
}const eventList = [{key: 'redirect',label: '跳转事件',event: events.redirect,param: '',},{key: 'alert',label: 'alert 事件',event: events.alert,param: '',},
]export {mixins,events,eventList,
}
不过不能在编辑的时候触发,可以在预览的时候触发。
添加事件
通过 v-for
指令将事件列表渲染出来:
<el-tabs v-model="eventActiveName"><el-tab-pane v-for="item in eventList" :key="item.key" :label="item.label" :><el-input v-if="item.key == 'redirect'" v-model="item.param" type="textarea" placeholder="请输入完整的 URL" /><el-input v-if="item.key == 'alert'" v-model="item.param" type="textarea" placeholder="请输入要 alert 的内容" /><el-button @click="addEvent(item.key, item.param)">确定</el-button></el-tab-pane>
</el-tabs>
选中事件时将事件添加到组件的 events
对象。
触发事件
预览或真正渲染页面时,也需要在每个组件外面套一层 DIV,这样就可以在 DIV 上绑定一个点击事件,点击时触发我们刚才添加的事件。
<template><div @click="handleClick"><component:is="config.component":style="getStyle(config.style)":propValue="config.propValue"/></div>
</template>
handleClick() {const events = this.config.eventsObject.keys(events).forEach(event => {this[event](events[event])})
}
11. 绑定动画
动画和事件的原理是一样的,先将所有的动画通过 v-for
指令渲染出来,然后点击动画将对应的动画添加到组件的 animations
数组里。同事件一样,执行的时候也是遍历组件所有的动画并执行。
为了方便,我们使用了 animate.css 动画库。
import '@/styles/animate.css'
现在我们提前定义好所有的动画数据:
export default [{label: '进入',children: [{ label: '渐显', value: 'fadeIn' },{ label: '向右进入', value: 'fadeInLeft' },{ label: '向左进入', value: 'fadeInRight' },{ label: '向上进入', value: 'fadeInUp' },{ label: '向下进入', value: 'fadeInDown' },{ label: '向右长距进入', value: 'fadeInLeftBig' },{ label: '向左长距进入', value: 'fadeInRightBig' },{ label: '向上长距进入', value: 'fadeInUpBig' },{ label: '向下长距进入', value: 'fadeInDownBig' },{ label: '旋转进入', value: 'rotateIn' },{ label: '左顺时针旋转', value: 'rotateInDownLeft' },{ label: '右逆时针旋转', value: 'rotateInDownRight' },{ label: '左逆时针旋转', value: 'rotateInUpLeft' },{ label: '右逆时针旋转', value: 'rotateInUpRight' },{ label: '弹入', value: 'bounceIn' },{ label: '向右弹入', value: 'bounceInLeft' },{ label: '向左弹入', value: 'bounceInRight' },{ label: '向上弹入', value: 'bounceInUp' },{ label: '向下弹入', value: 'bounceInDown' },{ label: '光速从右进入', value: 'lightSpeedInRight' },{ label: '光速从左进入', value: 'lightSpeedInLeft' },{ label: '光速从右退出', value: 'lightSpeedOutRight' },{ label: '光速从左退出', value: 'lightSpeedOutLeft' },{ label: 'Y轴旋转', value: 'flip' },{ label: '中心X轴旋转', value: 'flipInX' },{ label: '中心Y轴旋转', value: 'flipInY' },{ label: '左长半径旋转', value: 'rollIn' },{ label: '由小变大进入', value: 'zoomIn' },{ label: '左变大进入', value: 'zoomInLeft' },{ label: '右变大进入', value: 'zoomInRight' },{ label: '向上变大进入', value: 'zoomInUp' },{ label: '向下变大进入', value: 'zoomInDown' },{ label: '向右滑动展开', value: 'slideInLeft' },{ label: '向左滑动展开', value: 'slideInRight' },{ label: '向上滑动展开', value: 'slideInUp' },{ label: '向下滑动展开', value: 'slideInDown' },],},{label: '强调',children: [{ label: '弹跳', value: 'bounce' },{ label: '闪烁', value: 'flash' },{ label: '放大缩小', value: 'pulse' },{ label: '放大缩小弹簧', value: 'rubberBand' },{ label: '左右晃动', value: 'headShake' },{ label: '左右扇形摇摆', value: 'swing' },{ label: '放大晃动缩小', value: 'tada' },{ label: '扇形摇摆', value: 'wobble' },{ label: '左右上下晃动', value: 'jello' },{ label: 'Y轴旋转', value: 'flip' },],},{label: '退出',children: [{ label: '渐隐', value: 'fadeOut' },{ label: '向左退出', value: 'fadeOutLeft' },{ label: '向右退出', value: 'fadeOutRight' },{ label: '向上退出', value: 'fadeOutUp' },{ label: '向下退出', value: 'fadeOutDown' },{ label: '向左长距退出', value: 'fadeOutLeftBig' },{ label: '向右长距退出', value: 'fadeOutRightBig' },{ label: '向上长距退出', value: 'fadeOutUpBig' },{ label: '向下长距退出', value: 'fadeOutDownBig' },{ label: '旋转退出', value: 'rotateOut' },{ label: '左顺时针旋转', value: 'rotateOutDownLeft' },{ label: '右逆时针旋转', value: 'rotateOutDownRight' },{ label: '左逆时针旋转', value: 'rotateOutUpLeft' },{ label: '右逆时针旋转', value: 'rotateOutUpRight' },{ label: '弹出', value: 'bounceOut' },{ label: '向左弹出', value: 'bounceOutLeft' },{ label: '向右弹出', value: 'bounceOutRight' },{ label: '向上弹出', value: 'bounceOutUp' },{ label: '向下弹出', value: 'bounceOutDown' },{ label: '中心X轴旋转', value: 'flipOutX' },{ label: '中心Y轴旋转', value: 'flipOutY' },{ label: '左长半径旋转', value: 'rollOut' },{ label: '由小变大退出', value: 'zoomOut' },{ label: '左变大退出', value: 'zoomOutLeft' },{ label: '右变大退出', value: 'zoomOutRight' },{ label: '向上变大退出', value: 'zoomOutUp' },{ label: '向下变大退出', value: 'zoomOutDown' },{ label: '向左滑动收起', value: 'slideOutLeft' },{ label: '向右滑动收起', value: 'slideOutRight' },{ label: '向上滑动收起', value: 'slideOutUp' },{ label: '向下滑动收起', value: 'slideOutDown' },],},
]
然后用 v-for
指令渲染出来动画列表。
添加动画
<el-tabs v-model="animationActiveName"><el-tab-pane v-for="item in animationClassData" :key="item.label" :label="item.label" :><el-scrollbar><divv-for="(animate, index) in item.children":key="index"@mouseover="hoverPreviewAnimate = animate.value"@click="addAnimation(animate)"><div :class="[hoverPreviewAnimate === animate.value && animate.value + ' animated']">{{ animate.label }}</div></div></el-scrollbar></el-tab-pane>
</el-tabs>
点击动画将调用 addAnimation(animate)
将动画添加到组件的 animations
数组。
触发动画
运行动画的代码:
export default async function runAnimation($el, animations = []) {const play = (animation) => new Promise(resolve => {$el.classList.add(animation.value, 'animated')const removeAnimation = () => {$el.removeEventListener('animationend', removeAnimation)$el.removeEventListener('animationcancel', removeAnimation)$el.classList.remove(animation.value, 'animated')resolve()}$el.addEventListener('animationend', removeAnimation)$el.addEventListener('animationcancel', removeAnimation)})for (let i = 0, len = animations.length; i < len; i++) {await play(animations[i])}
}
运行动画需要两个参数:组件对应的 DOM 元素(在组件使用 this.$el
获取)和它的动画数据 animations
。并且需要监听 animationend
事件和 animationcancel
事件:一个是动画结束时触发,一个是动画意外终止时触发。
利用这一点再配合 Promise
一起使用,就可以逐个运行组件的每个动画了。
12. 导入 PSD
由于时间关系,这个功能我还没做。现在简单的描述一下怎么做这个功能。那就是使用 psd.js 库,它可以解析 PSD 文件。
使用 psd
库解析 PSD 文件得出的数据如下:
{ children: [ { type: 'group',visible: false,opacity: 1,blendingMode: 'normal',name: 'Version D',left: 0,right: 900,top: 0,bottom: 600,height: 600,width: 900,children: [ { type: 'layer',visible: true,opacity: 1,blendingMode: 'normal',name: 'Make a change and save.',left: 275,right: 636,top: 435,bottom: 466,height: 31,width: 361,mask: {},text: { value: 'Make a change and save.',font: { name: 'HelveticaNeue-Light',sizes: [ 33 ],colors: [ [ 85, 96, 110, 255 ] ],alignment: [ 'center' ] },left: 0,top: 0,right: 0,bottom: 0,transform: { xx: 1, xy: 0, yx: 0, yy: 1, tx: 456, ty: 459 } },image: {} } ] } ],document: { width: 900,height: 600,resources: { layerComps: [ { id: 692243163, name: 'Version A', capturedInfo: 1 },{ id: 725235304, name: 'Version B', capturedInfo: 1 },{ id: 730932877, name: 'Version C', capturedInfo: 1 } ],guides: [],slices: [] } } }
从以上代码可以发现,这些数据和 css 非常像。根据这一点,只需要写一个转换函数,将这些数据转换成我们组件所需的数据,就能实现 PSD 文件转成渲染组件的功能。目前 quark-h5 和 luban-h5 都是这样实现的 PSD 转换功能。
13. 手机模式
由于画布是可以调整大小的,我们可以使用 iphone6 的分辨率来开发手机页面。
这样开发出来的页面也可以在手机下正常浏览,但可能会有样式偏差。因为我自定义的三个组件是没有做适配的,如果你需要开发手机页面,那自定义组件必须使用移动端的 UI 组件库。或者自己开发移动端专用的自定义组件。
总结
由于 DEMO 的代码比较多,所以在讲解每一个功能点时,我只把关键代码贴上来。所以大家会发现 DEMO 的源码和我贴上来的代码会有些区别,请不必在意。
另外,DEMO 的样式也比较简陋,主要是最近事情比较多,没太多时间写好看点,请见谅。
参考资料
ref-line
quark-h5
luban-h5
易企秀
drag 事件
最后
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
欢迎加我微信「huab119」拉你进技术群,长期交流学习...
关注公众号「前端劝退师」,持续为你推送精选好文,也可以加我为好友,随时聊骚。
点个在看支持我吧,转发就更好了
可视化拖拽组件库一些技术要点原理分析相关推荐
- 可视化拖拽组件库一些技术要点原理分析(三)
本文是可视化拖拽系列的第三篇,之前的两篇文章一共对 17 个功能点的技术原理进行了分析: 编辑器 自定义组件 拖拽 删除组件.调整图层层级 放大缩小 撤消.重做 组件属性设置 吸附 预览.保存代码 绑 ...
- 可视化拖拽组件库一些技术要点原理分析(二)
本文是对<可视化拖拽组件库一些技术要点原理分析>[1]的补充.上一篇文章主要讲解了以下几个功能点: 1.编辑器2.自定义组件3.拖拽4.删除组件.调整图层层级5.放大缩小6.撤消.重做7. ...
- 最好用的 6 款 Vue 拖拽组件库推荐 - 卡拉云
本文首发:<最好用的 6 款 Vue 拖拽组件库推荐 - 卡拉云> Vue 拖拽组件库(drag-and-drop)组件在使用 Vue 框架开发中非常常见的需求,做个内容行排序,拖拽小组件 ...
- vue可视化拖拽组件模板,vue自定义下拉框组件
怎样利用Vue动态生成form表单 . $formCreate参数rules 表单生成规则[inputRule,selectRule,...]options 初始化配置参数(详细见底部createOp ...
- vue拖拽组件生成页面代码,vue可视化拖拽组件模板
怎么使用vuedraggable实现简单的拖拽,只拖拽一个内容(div) . 仔细观察发现,今日头条导航部分编辑效果,有以下几个效果1:点击编辑开启可以编辑的效果(也就是是否开启拖拽)2:拖拽前选中效 ...
- vue可视化拖拽组件模板_基于 Vue 丝滑般拖拽排序组件VueSlicksort
今天给大家分享一个功能超强的自由拖拽排序组件VueSlicksort. vue-slicksort 一款功能强大的可拖拽的vue.js组件.拥有丝滑般拖拽动画效果,支持水平/垂直/网格拖拽排序.还可以 ...
- vue可视化拖拽组件模板,vue组件拖拽自定义界面
前端可视化开发平台哪个好用? ThingJS 基于 HTML5 和 WebGL 技术,可方便地在主流浏览器上进行浏览和调试,支持 PC 和移动设备. ThingJS 为可视化应用提供了简单.丰富的功能 ...
- python可视化拖拽平台_【技术解码】不用控制器,只拖拽模型面就可以做动画!很好玩的动画工具...
文章转自公众号: cg世界 前两天在B站上看到一个有意思的视频,是新一代动画模型绑定控制技术的功能展示,小编看过之后挺好奇,就到网上找了一些相关介绍,想和小伙伴们分享一下. 这款3D动画软件名为Rum ...
- vue可视化拖拽生成工具_GitHub - 1260215278/dragUI: 基于vuedraggable.js + uni 的可视化拖拽编程,自动生成项目,自动生成代码,自行导入第三方组件...
dragUI 用于UNI可拖拽可视化编程 在线演示 效果图 基于 快速开始 参考uni官网安装普通uni项目(运行uni-app)运行到浏览器 另需要手动下载包npm install 项目目录 col ...
最新文章
- Nginx Kafka数据生产接口
- 何不用python_人生几何,何不Python当歌
- python字典的遍历方法_遍历python字典几种方法
- 一篇男人必看的创业文章。(人活着不能没有钱,但是活着却不能只为了钱)...
- python参数_python参数的介绍
- Visual studio 2012 ultimate 安装遇到 Prerequisites , 错误的函数 incorrect function
- 【Kafka】Kafka生产者producer相关参数详解batch.size linger.ms 等参数
- mysql到底可不可以使用join_《Mysql 到底可不可以使用 Join ?》
- 计算机二级access上机题,2017历年全国计算机二级access上机试题及答案
- Angularjs总结(一)表单验证
- 树立榜样、褒奖开源领域领袖人物、杰出贡献人物
- 用matlab画相频曲线_matlab 幅频特性 相频特性代码,图片
- PDM的工程图纸管理解决方案
- Latex改变section标题的编号方式
- 欢迎你、某某某同学python_python123第一周作业
- 如何理解数学公式中出现的极大极小minmax含义
- 小猫钓鱼java代码,【Java个人笔记】练习-小猫钓鱼
- Visustin(自动生成流程图)使用说明
- sql怎么撤回update_如何写好5000行的SQL代码
- 有属性的自定义注解,如何获取到post请求中RequestBody中对象的一个属性值?
热门文章
- java砍价算法逻辑,【拼多多算法数据库面试题】面试问题:拼多多砍价算法… - 看准网...
- python怎样算入门_Python基础练习之计算提成
- 在获取网页时半角全角字符混合的问题
- 2021-02-25(考古12年蓝桥省赛)
- 可爱卡通风PPT模板,专为老师教学打造的PPT模板-幼儿园PPT模板
- IntelliJ IDEA 自动导入包 关闭重复代码提示
- 让windows7学会定时关机
- Mac10.14安装Jdk11.04版本
- pytorch的深度Ritz方法入门
- jQuery实现表格的增加、修改、删除、保存。