react 可视化编辑器1
可视化编辑器1
前言
前面我们学习低代码,例如百度的低代码平台 amis,也有相应的可视化编辑器,通过拖拽的方式生成配置文件。就像这样
笔者自己也有类似需求
:比如中台有个归档需求,通过选择一些配置让后端执行一些操作。目前只有A项目要归档,过些日子B项目也需要归档,后面还有 C项目归档。如果不想每次来都重新编码,最好做一个编辑器,配置好数据,归档功能根据编辑器生成的配置 json 自动生成岂不美哉!
本篇将开始自己实现一个可视化编辑器。
Tip:环境采用 spug 项目,一个开源的 react 后台系统。
编辑器页面框架搭建
需求
:新建一个页面,有组件区、编辑区、属性区
效果如下图所示:
核心代码如下:
// spug\src\pages\lowcodeeditor\index.js
import React from 'react';
import { Layout } from 'antd';
import { observer } from 'mobx-react';
import styles from './style.module.less'
const { Header, Footer, Sider, Content } = Layout;
export default observer(function () {
return (
<Layout className={styles.box}>
<Sider width='400' className={styles.componentBox}>组件区</Sider>
<Layout>
<Header className={styles.editorMenuBox}>菜单区</Header>
<Content className={styles.editorBox}>编辑区</Content>
</Layout>
<Sider width='400' className={styles.attributeBox}>属性区</Sider>
</Layout>
)
})
Tip: 直接用 antd 中的 layout 布局。
// spug\src\pages\lowcodeeditor\style.module.less
.box{
min-width: 1500px;
// color: pink;
font-size: 2em;
// 组件盒子
.componentBox{
background-color: blue;
}
// 编辑器菜单
.editorMenuBox{
background-color: pink;
}
// 编辑器盒子
.editorBox{
background-color: red;
}
// 属性盒子
.attributeBox{
background-color: yellow;
}
}
根据配置文件渲染编辑区
需求
:根据配置文件渲染编辑区
通常是从组件区拖拽组件到编辑区,编辑区就会显示该组件,这里先不管拖拽,直接通过定义数据,编辑区根据数据渲染。
这里采用绝对定位的玩法,你也可以选择其他的,比如拖拽到编辑器后释放,则渲染该组件,不支持拖动,就像 amis 编辑器。
思路:
- 将编辑区提取出单独模块 Container.js,完成对应css效果
- 新建数据配置文件 data.js,存放页面配置数据
- Container 根据配置数据将组件渲染到编辑区
Tip:最初打算将配置文件改为 .json 文件,后来发现缺少对应 loader 无法导入import data from './data.json'
。
效果如下图所示
核心代码如下:
将容器抽取成单独组件:
// spug\src\pages\lowcodeeditor\Container.js
import React from 'react';
import { observer } from 'mobx-react';
import styles from './style.module.less'
import data from './data.js'
import store from './store';
import ComponentBlock from './ComponentBlock';
@observer
class Container extends React.Component {
componentDidMount() {
// 初始化
store.json = data
}
render() {
const {container = {}, components} = store.json || {};
const {width, height} = container
return (
<div className={styles.containerBox}>
<div className={styles.container} style={{width, height}}>
<ComponentBlock/>
</div>
</div>
)
}
}
export default Container
容器对应的样式:
// spug\src\pages\lowcodeeditor\style.module.less
.box{
...
// 容器盒子
.containerBox{
height: 100%;
border: 1px solid red;
padding: 5px;
// 容器的宽度高度有可能很大
overflow: auto;
}
// 容器
.container{
// 容器中的组件需要相对容器定位
position: relative;
margin:0 auto;
background: rgb(202, 199, 199);
border: 2px solid orange;
// 容器中渲染的组件
.componentBlock{
position: absolute;
}
}
}
配置数据格式如下:
// spug\src\pages\lowcodeeditor\data.js
const data = {
container: {
width: "800px",
height: "600px"
},
components: [
{top: 100, left: 300, 'zIndex': 1, type: 'text'},
{top: 200, left: 200, 'zIndex': 1, type: 'input'},
{top: 300, left: 100, 'zIndex': 1, type: 'button'},
]
}
export default data
组件块模块:
// spug\src\pages\lowcodeeditor\ComponentBlocks.js
import React, {Fragment} from 'react';
import { observer } from 'mobx-react';
import styles from './style.module.less'
import store from './store';
@observer
class ComponentBlock extends React.Component {
componentDidMount() {
// 初始化
}
render() {
const { components} = store.json || {};
return (
<Fragment>
{
components?.map(item => <p className={styles.componentBlock} style={{left: item.left, top: item.top}}>我的类型是: {item.type}</p>)
}
</Fragment>
)
}
}
export default ComponentBlock
直接引入容器组件(Container):
// spug\src\pages\lowcodeeditor\index.js
...
import Container from './Container'
export default observer(function () {
return (
<Layout className={styles.box}>
<Layout>
<Header className={styles.editorMenuBox}>菜单区</Header>
<Content className={styles.editorBox}>
<Container/>
</Content>
</Layout>
</Layout>
)
})
将配置数据放入 store:
// spug\src\pages\lowcodeeditor\store.js
import { observable, computed } from 'mobx';
class Store {
// 配置数据
@observable json = null;
}
export default new Store()
物料区组件渲染
上面我们根据一些假数据,实现能根据位置渲染内容
编辑区现在渲染的三个组件都是文本,我们期望的是正确的text
、input
和button
组件。
需求
:物料区渲染组件、编辑区渲染对应的三个组件
效果如下图所示:
核心代码如下:
编辑区提取出单独的模块:
// spug\src\pages\lowcodeeditor\index.js
import Material from './Material'
...
export default observer(function () {
return (
<Layout className={styles.box}>
<Sider width='400' className={styles.componentBox}>
<Material/>
</Sider>
</Layout>
)
})
物料区(即组件区)注册三个组件:
// 物料区(即组件区)
// spug\src\pages\lowcodeeditor\Material.js
import React, { Fragment } from 'react';
import { observer } from 'mobx-react';
import { Input, Button, Tag } from 'antd';
import styles from './style.module.less'
import store from './store';
@observer
class Material extends React.Component {
componentDidMount() {
// 初始化
this.registerMaterial()
}
// 注册物料
registerMaterial = () => {
store.registerMaterial({
// 组件类型
key: 'text',
// 组件文本
label: '文本',
// 组件预览。函数以便传达参数进来
preview: () => '预览文本',
// 组件渲染
render: () => '渲染文本',
})
store.registerMaterial({
key: 'button',
label: '按钮',
preview: () => <Button type="primary">预览按钮</Button>,
render: () => <Button type="primary">渲染按钮</Button>,
})
store.registerMaterial({
key: 'input',
label: '输入框',
preview: () => <Input style={{ width: '50%', }} placeholder='预览输入框' />,
render: () => <Input placeholder='渲染输入框' />,
})
}
render() {
return (
<Fragment>
{
store.componentList.map((item, index) =>
<section className={styles.combomentBlock} key={index}>
{/* 文案 */}
<Tag className={styles.combomentBlockLabel} color="cyan">{item.label}</Tag>
{/* 组件渲染 */}
<div className={styles.combomentBlockBox}>{item.preview()}</div>
</section>)
}
</Fragment>
)
}
}
export default Material
编辑区调整一下,根据 store.componentMap 渲染对应组件:
// spug\src\pages\lowcodeeditor\ComponentBlocks.js
@observer
class ComponentBlock extends React.Component {
render() {
const { components } = store.json || {};
return (
<Fragment>
{
components?.map(item =>
<div style={{ position: 'absolute', left: item.left, top: item.top }}>{store.componentMap[item.type].render()}</div>
)
}
</Fragment>
)
}
}
物料的数据都放入状态管理模块中:
// spug\src\pages\lowcodeeditor\store.js
import { observable, computed } from 'mobx';
class Store {
// 配置数据
@observable json = null;
@observable componentList = []
@observable componentMap = {}
// 注册物料
registerMaterial = (item) => {
this.componentList.push(item)
this.componentMap[item.key] = item
}
}
export default new Store()
对应样式:
// spug\src\pages\lowcodeeditor\style.module.less
.box{
min-width: 1500px;
// 组件盒子
.componentBox{
background-color: #fff;
// 组件块
.combomentBlock{
position: relative;
margin: 10px;
border: 1px solid #95de64;
.combomentBlockLabel{
position: absolute;
left:0;
top:0;
}
.combomentBlockBox{
display: flex;
align-items: center;
justify-content: center;
height: 100px;
}
}
// 组件块上添加一个蒙版,防止用户点击组件
.combomentBlock::after{
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0,0,0,.05);
// 增加移动效果
cursor: move;
}
}
...
物料组件拖拽到编辑区
需求
:将物料区的组件拖拽到编辑区,并在鼠标释放的地方渲染该组件
效果如下图所示:
物料区的组件需要增加 draggable
属性就可以拖动了。就像这样:
<section className={styles.combomentBlock} key={index}
// 元素可以拖拽
draggable
>
</section>)
效果如下:
核心代码如下:
修改物料组件,让物料组件支持拖拽,并在拖拽前记录拖动的组件。
// 物料区(即组件区)
// spug\src\pages\lowcodeeditor\Material.js
@observer
class Material extends React.Component {
// 记录拖动的元素
dragstartHander = (e, target) => {
// 标记正在拖拽
store.dragging = true;
// 记录拖拽的组件
store.currentDragedCompoent = target
// {"key":"text","label":"文本"}
// console.log(JSON.stringify(target))
}
render() {
return (
<Fragment>
{
store.componentList.map((item, index) =>
<section className={styles.combomentBlock} key={index}
// 元素可以拖拽
draggable
onDragStart={e => this.dragstartHander(e, item)}
>
{/* 文案 */}
<Tag className={styles.combomentBlockLabel} color="cyan">{item.label}</Tag>
{/* 组件预览 */}
<div className={styles.combomentBlockBox}>{item.preview()}</div>
</section>)
}
</Fragment>
)
}
}
export default Material
容器组件中初始化配置文件,默认容器大小是800*600,并给容器增加 dragover 和 drop 事件,并在 drop 事件里新增组件到数据中。
// spug\src\pages\lowcodeeditor\Container.js
import React from 'react';
import { observer } from 'mobx-react';
import styles from './style.module.less'
import store from './store';
import ComponentBlocks from './ComponentBlocks';
@observer
class Container extends React.Component {
componentDidMount() {
// 初始化
store.json = {
container: {
width: "800px",
height: "600px"
},
components: [
// {top: 100, left: 300, 'zIndex': 1, type: 'text'},
]
}
}
// 如果不阻止默认行为,则不会触发 drop,进入容器后也不会出现移动标识
dragOverHander = e => {
e.preventDefault()
}
// 新增组件到数据中
dropHander = e => {
store.dragging = false;
// e 中没有offsetX,到原始事件中找到 offsetX。
const { nativeEvent = {} } = e;
const component = {
top: nativeEvent.offsetY,
left: nativeEvent.offsetX,
zIndex: 1,
type: store.currentDragedCompoent.type
};
// 重置
store.currentDragedCompoent = null;
// 添加组件
store.json.components.push(component)
}
render() {
const { container = {}, components } = store.json || {};
const { width, height } = container
return (
<div className={styles.containerBox}>
<div className={styles.container} style={{ width, height, }}
onDragOver={this.dragOverHander}
onDrop={this.dropHander}
>
<ComponentBlocks />
</div>
</div>
)
}
}
export default Container
物料组件拖拽到编辑区,释放鼠标时需要在该位置渲染组件,而 offerX 会相对容器中的子元素定位,这里使用 pointer-events
解决:
// spug\src\pages\lowcodeeditor\ComponentBlocks.js
@observer
class ComponentBlocks extends React.Component {
render() {
const { components } = store.json || {};
return (
<Fragment>
{
components?.map((item, index) =>
// `pointer-events: none` 解决offsetX 穿透子元素的问题。
// pointer-events 兼容性高达98%以上
<div key={index} style={{ pointerEvents: store.dragging ? 'none' : 'auto', position: 'absolute', left: item.left, top: item.top }}>{store.componentMap[item.type]?.render()}</div>
)
}
</Fragment>
)
}
}
store 中新增数据方法如下:
// spug\src\pages\lowcodeeditor\store.js
class Store {
@observable dragging = false;
// 记录当前拖动的组件,drop 时置空
@observable currentDragedCompoent = null
// 注册物料
registerMaterial = (item) => {
this.componentList.push(item)
this.componentMap[item.type] = item
}
...
}
export default new Store()
相关知识点:
- dragenter - 当拖动的元素或被选择的文本进入有效的放置目标时, dragenter 事件被触发。
- dragover - 当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover 事件(每几百毫秒触发一次)
- dragleave - 事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发。
- drop - 事件在元素或选中的文本被放置在有效的放置目标上时被触发。
- DataTransfer.dropEffect 属性控制在拖放操作中给用户的反馈(通常是视觉上的)。它会影响在拖拽过程中光标的手势。笔者设置了
e.dataTransfer.dropEffect = "move"
也没效果 - offsetX - 规定了事件对象与目标节点的内填充边(padding edge)在 X 轴方向上的偏移量。会相对子元素,笔者采用
pointer-events: none
解决offsetX 穿透子元素的问题。
Tip:screenX、clientX、pageX, offsetX的区别如下图(来自网友 Demi
)所示
还有一个小问题:物料拖拽到编辑区释放时,新组件的左上角(left,top)正好是释放时鼠标的位置。
笔者期望鼠标释放的位置是新组件的正中心。
增加 translate(-50%, -50%)
即可。就像这样
// spug\src\pages\lowcodeeditor\ComponentBlocks.js
<div key={index} style={{
...
// 鼠标释放的位置是新组件的正中心
transform: `translate(-50%, -50%)`
}}>...</div>
不过这个方案在下面实现辅助线对齐时却遇到问题,需要替换方案。
编辑区的组件拖拽
这里分三步:首先是选中组件,然后拖拽选中的组件,最后添加对齐辅助线功能。
编辑区的组件选中
需求
:编辑区的组件支持选中。比如:
- 直接选中某组件就直接拖动(不释放鼠标)
- 仅选中某组件
- 选择多个组件,例如按住 shift
效果如下图所示:
这里用 mousedown 事件。核心代码如下:
// spug\src\pages\lowcodeeditor\ComponentBlocks.js
@observer
class ComponentBlocks extends React.Component {
mouseDownHandler = (e, target) => {
// 例如防止点击 input 时,input 会触发 focus 聚焦。
e.preventDefault()
// 如果按下 shift 则只处理单个
if (e.shiftKey) {
target.focus = !target.focus
} else if (!target.focus) {
// 清除所有选中
store.json.components.forEach(item => item.focus = false)
target.focus = true
} else {
// 这里无需清除所有选中
// 注销这句话拖动效果更好。
// target.focus = false
}
console.log(target)
}
render() {
const { components } = store.json || {};
return (
<Fragment>
{
components?.map((item, index) =>
<div key={index}
className={styles.containerBlockBox}
style={{
...
// 选中效果
border: item.focus ? '1.5px dashed red' : 'none',
}}
onMouseDown={e => this.mouseDownHandler(e, item)}
>...</div>
)
}
</Fragment>
)
}
}
export default ComponentBlocks
添加样式:
// spug\src\pages\lowcodeeditor\style.module.less
// 容器
.container{
...
// 编辑区的组件上添加一个蒙版,防止用户点击组件
.containerBlockBox::after{
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
拖拽编辑区组件
需求
:编辑区内的选中的组件支持拖拽
思路:
- 点击容器,应取消所有选中组件
- mousedown时记录点击的开始位置坐标,移动时计算偏移量并更新组件位置
核心代码如下:
// spug\src\pages\lowcodeeditor\Container.js
@observer
class Container extends React.Component {
...
// 点击容器,取消所有选中
clickHander = e => {
e.target.className.split(/\s+/).includes('container') &&
// 清除所有选中
store.json.components.forEach(item => item.focus = false)
}
mouseDownHandler = e => {
// 开始坐标
this.startCoordinate = {
x: e.pageX,
y: e.pageY
}
console.log('down。记录位置:', this.startCoordinate)
}
mouseMoveHander = e => {
if(!this.startCoordinate){
return
}
const {pageX, pageY} = e
const {x, y} = this.startCoordinate
// 移动的距离
const moveX = pageX - x
const moveY = pageY - y
console.log('move。更新位置。移动坐标:', {moveX, moveY})
store.focusComponents.forEach(item => {
item.top = item.top + moveY;
item.left = item.left + moveX;
})
// 更新开始位置。
this.startCoordinate = {
x: e.pageX,
y: e.pageY
}
}
mouseUpHander = e => {
console.log('up')
this.startCoordinate = null
}
render() {
const { container = {}, components } = store.json || {};
const { width, height } = container
return (
<div className={styles.containerBox}>
{/* 多个 className */}
<div className={`container ${styles.container}`} style={{ width, height, }}
...
onClick={this.clickHander}
onMouseDown={e => this.mouseDownHandler(e)}
onMouseMove={e => this.mouseMoveHander(e)}
onMouseUp={e => this.mouseUpHander(e)}
>
<ComponentBlocks />
</div>
</div>
)
}
}
export default Container
// spug\src\pages\lowcodeeditor\store.js
class Store {
// 配置数据
@observable json = null;
// 获取 json 中选中的项
@computed get focusComponents() {
return this.json.components.filter(item => item.focus)
}
// 获取 json 中未选中的项
@computed get unFocusComponents() {
return this.json.components.filter(item => !item.focus)
}
}
辅助线对齐
需求
:增加对齐辅助线。比如讲文本组件和按钮组件顶对齐、底对齐、居中对齐等等。靠近辅助线时也能自动贴上去。
效果如下图所示:
思路:
- 记录未选中元素的辅助线的位置以及显现该辅助线的坐标
- 当前选中元素的坐标如果和辅助线的坐标匹配则显示对应辅助线
- 同时拖动多个元素,以最后选中的元素为准
每个未选中的元素有10种情况。请看下图:
A 表示未选中元素,B表示选中拖动的元素,A 有三条辅助线,从上到下5种情况下显示:
- A顶对B底
- A顶对B顶
- A中对B中
- A底对B底
- A底对B顶
这里是水平方向5中情况,垂直方向也有5种情况。
核心代码如下:
首先记录最后一个选中的元素。核心代码如下:
在 store.js 中增加如下相关变量:
- startCoordinate - 作用:1. 记录开始坐标位置,用于计算移动偏移量 2. 松开鼠标后,辅助线消失
- guide - 存放辅助线
- adjacency - 拖拽的组件靠近辅助线时(2px内),辅助线出现
- lastSelectedElement - 最后选中的元素
- adjacencyGuides - 紧邻的辅助线,即要显示的辅助线
// spug\src\pages\lowcodeeditor\store.js
import { observable, computed } from 'mobx';
class Store {
// 开始坐标。作用:1. 记录开始坐标位置,用于计算移动偏移量 2. 松开鼠标后,辅助线消失
@observable startCoordinate = null
// 辅助线。xArray 存储垂直方向的辅助线;yArray 存储水平方向的辅助线;
@observable guide = { xArray: [], yArray: [] }
// 拖拽的组件靠近辅助线时(2px内),辅助线出现
@observable adjacency = 2
// 最后选中的元素索引。用于辅助线
@observable lastSelectedIndex = -1
// 最后选中的元素
@computed get lastSelectedElement() {
return this.json.components[this.lastSelectedIndex]
}
// 紧邻的辅助线,即要显示的辅助线
@computed get adjacencyGuides() {
return this.lastSelectedElement && this.guide.yArray
// 相对元素坐标与靠近辅助线时,辅助线出现
?.filter(item => Math.abs(item.y - this.lastSelectedElement.top) <= this.adjacency)
}
}
export default new Store()
ComponentBlocks.js 改动如下:
- 将子组件的渲染抽离成一个单独组件ComponentBlock。主要用于获取组件的宽度和高度。组件得渲染后才知晓其尺寸
- mousedown 时记录鼠标位置以及初始化辅助线
- 辅助线有水平方向和垂直方向,分别存入 yArray 和 xArray 中。其数据结构为
{showTop: xx, y: xx}
。笔者只实现了水平方向的辅助线。例如A顶(未选中元素)对B底
的 y 的计算方式请看下图:
- 将组件的选中样式从 border 改为不占空间的
outline
,防止取消选中时组件的抖动。 - 鼠标释放的位置是新组件的正中心方案修改。第一次从物料区拖拽组件释放时修改坐标。
// spug\src\pages\lowcodeeditor\ComponentBlocks.js
...
@observer
class ComponentBlocks extends React.Component {
mouseDownHandler = (e, target, index) => {
...
// 记录开始位置
store.startCoordinate = {
x: e.pageX,
y: e.pageY
}
...
// 初始化辅助线。选中就初始化辅助线,取消不管。
this.initGuide(target, index)
}
// 初始化辅助线。
// 注:仅完成水平辅助线,垂直辅助线请自行完成。
initGuide = (component, index) => {
// 记录最后一个选中元素的索引
// 问题:依次选中1个、2个、3个元素,然后取消第3个元素的选中,这时最后一个元素的索引依然指向第三个元素,这就不正确了。会导致辅助线中相对最后一个选中元素不正确。
// 解决办法:通过定义变量(store.startCoordinate)来解决此问题
store.lastSelectedIndex = component.focus ? index : -1
if (!component.focus) {
return
}
// console.log('初始化辅助线')
store.guide = { xArray: [], yArray: [] }
store.unFocusComponents.forEach(item => {
const { xArray: x, yArray: y } = store.guide
// 相对元素。即选中的最后一个元素
const { lastSelectedElement: relativeElement } = store
// A顶(未选中元素)对B底
// showTop 辅助线出现位置。y - 相对元素的 top 值为 y 时辅助线将显现
y.push({ showTop: item.top, y: item.top - relativeElement.height })
// A顶对B顶
y.push({ showTop: item.top, y: item.top })
// A中对B中
y.push({ showTop: item.top + item.height / 2, y: item.top + (item.height - relativeElement.height) / 2 })
// A底对B底
y.push({ showTop: item.top + item.height, y: item.top + item.height - relativeElement.height })
// A底对B顶
y.push({ showTop: item.top + item.height, y: item.top + item.height })
})
}
render() {
const { components } = store.json || {};
return (
<Fragment>
{
components?.map((item, index) =>
<ComponentBlock key={index} index={index} item={item} mouseDownHandler={this.mouseDownHandler} />
)
}
</Fragment>
)
}
}
// 必须加上 @observer
// 将子组件拆分用于设置组件的宽度和高度
@observer
class ComponentBlock extends React.Component {
constructor(props) {
super(props)
this.box = React.createRef()
}
componentDidMount() {
// 初始化组件的宽度和高度
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
const { offsetWidth, offsetHeight } = this.box.current
const component = store.json.components[this.props.index] ?? {}
component.width = offsetWidth
component.height = offsetHeight
// 组件第一次从物料区拖拽到编辑区,将组件中心位置设置为释放鼠标位置。
// transform: `translate(-50%, -50%)` 的替代方案
if (component.isFromMaterial) {
component.isFromMaterial = false
component.left = component.left - (component.width) / 2
component.top = component.top - (component.height) / 2
}
}
render() {
const { index, item, mouseDownHandler } = this.props;
return (
<div ref={this.box}
className={styles.containerBlockBox}
style={{
...
// 选中效果
// border 改 outline(轮廓不占据空间)。否则取消元素选中会因border消失而抖动
outline: item.focus ? '1.5px dashed red' : 'none',
// 鼠标释放的位置是新组件的正中心
// transform: `translate(-50%, -50%)`,
}}
onMouseDown={e => mouseDownHandler(e, item, index)}
>{store.componentMap[item.type]?.render()}</div>
)
}
}
export default ComponentBlocks
Container.js 主要改动:
- isFromMaterial - 鼠标释放的位置是新组件的正中心的一个标记
- adjacencyGuideOffset - 自动贴近辅助线的偏移量。体验有些问题,默认关闭
- mouseUpHander - mouseup 后辅助线不在显示
- render() - 增加辅助线的渲染
// spug\src\pages\lowcodeeditor\Container.js
...
@observer
class Container extends React.Component {
...
dropHander = e => {
...
const component = {
// 从物料拖拽到编辑器。在编辑器初次显示后则关闭。
+ isFromMaterial: true,
top: nativeEvent.offsetY,
left: nativeEvent.offsetX,
zIndex: 1,
type: store.currentDragedCompoent.type
};
...
}
// 自动贴近辅助线的偏移量
// 体验有些问题,默认关闭。即贴近后,移动得慢会导致元素挪不开,因为自动贴近总会执行
// 注:只实现了Y轴(水平辅助线)
adjacencyGuideOffset = (close = true) => {
const result = { offsetY: 0, offsetX: 0 }
if (close) {
return result
}
// 取得接近的辅助线
const adjacencyGuide = store.guide.yArray
// 拖拽的组件靠近辅助线时(2px内),辅助线出现
.find(item => Math.abs(item.y - store.lastSelectedElement.top) <= store.adjacency)
if (adjacencyGuide) {
// 体验不好:取消贴近辅助线功能
result.offsetY = adjacencyGuide.y - store.lastSelectedElement.top;
}
return result
}
mouseMoveHander = e => {
// 选中元素后,再移动才有效
if (!store.startCoordinate) {
return
}
// 上个位置
const { x, y } = store.startCoordinate
let { pageX: newX, pageY: newY } = e
// 自动贴近偏移量。默认关闭此功能。
const { offsetY: autoApproachOffsetY, offsetX: autoApproachOffsetX } = this.adjacencyGuideOffset()
// 移动的距离
const moveX = newX - x + autoApproachOffsetX
const moveY = newY - y + autoApproachOffsetY
// console.log('move。更新位置。移动坐标:', {moveX, moveY})
store.focusComponents.forEach(item => {
item.left += moveX
item.top += moveY
})
// 更新开始位置。
store.startCoordinate = {
x: newX,
y: newY
}
}
// mouseup 后辅助线不在显示
mouseUpHander = e => {
store.startCoordinate = null
}
render() {
...
return (
<div className={styles.containerBox}>
<div>
<ComponentBlocks />
{/* 辅助线 */}
{
store.startCoordinate && store.adjacencyGuides
?.map((item, index) => {
return <i key={index} className={styles.guide} style={{ top: item.showTop }}></i>
})
}
</div>
</div>
)
}
}
export default Container
辅助线样式:
// spug\src\pages\lowcodeeditor\style.module.less
// 辅助线
.guide{
position: absolute;
width: 100%;
border-top: 1px dashed red;
}
react 可视化编辑器1的更多相关文章
- Vue + element从零打造一个H5页面可视化编辑器——pl-drag-template
pl-drag-template Github地址:https://github.com/livelyPeng/pl-drag-template 前言 想必你一定使用过易企秀或百度H5等微场景生成工具 ...
- React 可视化开发工具 Shadow Widget 非正经入门(之六:markdown)
本系列博文从 Shadow Widget 作者的视角,解释该框架的设计要点.本篇讲解 Markdown 在 Shadow Widget 中的应用. Markdown 在 Shadow Widget 中 ...
- rails使用bootstrap3-wysiwyg可视化编辑器并实现自定义图片上传插入功能
之前在rails开发中使用了ckeditor作为可视化编辑器,不过感觉ckeditor过于庞大,有很多不需要的功能,而且图片上传功能不好控制不同用户可以互相删除图片,感觉很不好.于是考虑更改可视化编辑 ...
- 使用wp_editor函数实现可视化编辑器
在最近的wp项目中遇到了需要使用可视化编辑器来接收用户的输入,正好就研究了一下wp_editor这个函数的用法,利用这个函数能很方便的把textarea文本域变成可视化编辑器. Wp_editor函数 ...
- 先进的React GUI编辑器 — Structor,所见即所得!
先进的React GUI编辑器 — Structor https://helmetrex.com/ 手把手教你基于 ES6 架构自己的 React Boilerplate 项目 http://www. ...
- myeclipse关闭html,jsp等页面的可视化编辑器
myeclipse打开html,jsp等页面时,有的是默认用可视化编辑器打开的,这样打开会显得很慢,只要关闭可视化编辑器就会快很多了,方法如下: 1,选择菜单: windows -> prefe ...
- android窗口泄漏,isInEditMode解决可视化编辑器无法识别自定义控件的问题
android窗口泄漏 在做项目是遇到这个错误:google:WindowManager: Activity has leaked window. 产 生原因:我们知道Android的每一个Activ ...
- 10 个免费的 jQuery 可视化编辑器插件
富文本编辑器,也就是所见即所得的 HTML 编辑器,是网站一个非常重要的组件,特别是对于一些内容发布网站来说.本文介绍 10 个基于 jQuery 的可视化文本编辑器. MarkitUp markIt ...
- 使用isInEditMode解决可视化编辑器无法识别自定义控件的问题
如果在自定义控件的构造函数或者其他绘制相关地方使用系统依赖的代码, 会导致可视化编辑器无法报错并提示:Use View.isInEditMode() in your custom views to s ...
随机推荐
- OS模块中获取当前文件的绝对路径的相关方法
os.path.realpath(__file__) 作用:获取当前执行py脚本的绝对路径(在当前工作目录下的绝对路径) __file__ : 表示当前文件的本身,一般值是当前文件的相对路径 例如: ...
- 正则表达式实战:最新豆瓣top250爬虫超详细教程
检查网页源代码 首先让我们来检查豆瓣top250的源代码,一切网页爬虫都需要从这里开始.F12打开开发者模式,在元素(element)页面通过Ctrl+F直接搜索你想要爬取的内容,然后就可以开始编写正 ...
- Taurus.MVC 微服务框架 入门开发教程:项目部署:3、微服务应用程序版本升级:全站升级和局部模块升级。
系列目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 本系列第一篇:Taurus.MVC V3.0.3 微服务开源框架发布:让.NET 架构在大并发的演进过程更简单 ...
- vsftpd 操作手册 - 完整版
vsftpd # 目录 - ftp 简介 - vsftpd 简介 - vsftpd 安装&卸载 - vsftpd 配置文件 - vsftpd 认证模板 - vsftpd 配置模板 # 目录详解 ...
- RestTemplate用法
RestTemplate 用法 RestTemplate简介 RestTemplate 是一个同步的web http客户端请求模板工具,spring框架做的抽象模板, 常见的http客户端请求工具有: ...
- 【Matlab】学习记录1-简单的函数介绍
sind(30) %正弦函数,以角度为单位 ans =0.5000 exp(2) %以e为底的指数函数,即e^x ans =7.3891 log10(10) ans =1log(exp(1)) ...
- python数据精度问题
一.python运算时精度问题: 1.运行时精度问题在Python中(其他语言中也存在这个问题,这是计算机采用二进制导致的),有时候由于二进制和十进制之间对应问题会导致数值的精度问题,比如无法用有限个 ...
- day29--Java泛型02
Java泛型02 5.自定义泛型 5.1自定义泛型类 基本语法: class 类名<T,R...>{//-表示可以有多个泛型 成员 } 注意细节: 普通成员可以使用泛型(属性.方法) 使用 ...
- Linux有趣命令
通外网 下载使用阿里云镜像源:wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.re ...
- KingbaseFlySync ddl变更流程
关键字: KingbaseFlySync.Linux.x86_64.mips64el.aarch64.Java 一.ddl变更流程 1. 停掉客户业务,保证没有新数据产生 确认Oracle数据库上所有 ...