可视化编辑器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()

物料区组件渲染

上面我们根据一些假数据,实现能根据位置渲染内容

编辑区现在渲染的三个组件都是文本,我们期望的是正确的textinputbutton组件。

需求:物料区渲染组件、编辑区渲染对应的三个组件

效果如下图所示:

核心代码如下:

编辑区提取出单独的模块:

// 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的更多相关文章

  1. Vue + element从零打造一个H5页面可视化编辑器——pl-drag-template

    pl-drag-template Github地址:https://github.com/livelyPeng/pl-drag-template 前言 想必你一定使用过易企秀或百度H5等微场景生成工具 ...

  2. React 可视化开发工具 Shadow Widget 非正经入门(之六:markdown)

    本系列博文从 Shadow Widget 作者的视角,解释该框架的设计要点.本篇讲解 Markdown 在 Shadow Widget 中的应用. Markdown 在 Shadow Widget 中 ...

  3. rails使用bootstrap3-wysiwyg可视化编辑器并实现自定义图片上传插入功能

    之前在rails开发中使用了ckeditor作为可视化编辑器,不过感觉ckeditor过于庞大,有很多不需要的功能,而且图片上传功能不好控制不同用户可以互相删除图片,感觉很不好.于是考虑更改可视化编辑 ...

  4. 使用wp_editor函数实现可视化编辑器

    在最近的wp项目中遇到了需要使用可视化编辑器来接收用户的输入,正好就研究了一下wp_editor这个函数的用法,利用这个函数能很方便的把textarea文本域变成可视化编辑器. Wp_editor函数 ...

  5. 先进的React GUI编辑器 — Structor,所见即所得!

    先进的React GUI编辑器 — Structor https://helmetrex.com/ 手把手教你基于 ES6 架构自己的 React Boilerplate 项目 http://www. ...

  6. myeclipse关闭html,jsp等页面的可视化编辑器

    myeclipse打开html,jsp等页面时,有的是默认用可视化编辑器打开的,这样打开会显得很慢,只要关闭可视化编辑器就会快很多了,方法如下: 1,选择菜单: windows -> prefe ...

  7. android窗口泄漏,isInEditMode解决可视化编辑器无法识别自定义控件的问题

    android窗口泄漏 在做项目是遇到这个错误:google:WindowManager: Activity has leaked window. 产 生原因:我们知道Android的每一个Activ ...

  8. 10 个免费的 jQuery 可视化编辑器插件

    富文本编辑器,也就是所见即所得的 HTML 编辑器,是网站一个非常重要的组件,特别是对于一些内容发布网站来说.本文介绍 10 个基于 jQuery 的可视化文本编辑器. MarkitUp markIt ...

  9. 使用isInEditMode解决可视化编辑器无法识别自定义控件的问题

    如果在自定义控件的构造函数或者其他绘制相关地方使用系统依赖的代码, 会导致可视化编辑器无法报错并提示:Use View.isInEditMode() in your custom views to s ...

随机推荐

  1. 【Azure 事件中心】使用Azure AD认证方式创建Event Hub Consume Client + 自定义Event Position

    问题描述 当使用SDK连接到Azure Event Hub时,最常规的方式为使用连接字符串.这种做法参考官网文档就可成功完成代码:https://docs.azure.cn/zh-cn/event-h ...

  2. 羽夏看Linux内核——启动那些事

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后面,并 ...

  3. 论文解读(g-U-Nets)《Graph U-Nets》

    论文信息 论文标题:Graph U-Nets论文作者:Hongyang Gao, Shuiwang Ji论文来源:2019,ICML论文地址:download 论文代码:download 1 Intr ...

  4. 让 DolphinScheduler 1.3.4 开启 Netty 日志打印,解决流程实例一直在运行中的问题

    关于新一代大数据任务调度 - Apache DolphinScheduler   Apache DolphinScheduler(incubator) 于 17 年在易观数科立项, 19 年 8 月进 ...

  5. java-Servlet-cookie and session

    1 状态管理 a) 什么是状态管理?将浏览器与web服务器之间多次交互当做一个整体处理,并且将多次交互涉及的数据(即状态)保存下来. b) 如何进行状态管理? 可以将状态保存在客户端将状态保存到浏览器 ...

  6. Blazor VS Vue

    Vue--​​两分钟概述 Vue 是一个JavaScript 框架. 在其最简单的模式中,您可以简单地将核心 Vue 脚本包含在您的应用程序中,然后开始构建您的组件. 除此之外,对于更复杂的应用程序, ...

  7. docker compose搭建redis7.0.4高可用一主二从三哨兵集群并整合SpringBoot【图文完整版】

    一.前言 redis在我们企业级开发中是很常见的,但是单个redis不能保证我们的稳定使用,所以我们要建立一个集群. redis有两种高可用的方案: High availability with Re ...

  8. 使用 Vue.js 框架后的感想

    前言 用 Vue 已经有段时间了,把自己的所想所悟写下来,每一个想法都是非常宝贵的,记录成为生活,记录成为习惯. 简化开发 Vue 是可以辅助前端工程师开发 Web App 的一种框架,它节省很多时间 ...

  9. 探秘:TriCore处理器中断机制

    1. TriCore与中断的简介 TriCore是德国英飞凌科技公司旗下的第一个为实时嵌入式系统而优化的统一的.32位的微控制器-DSP(Digital Signal Processing)处理器架构 ...

  10. oracle 怎么查看用户对应的表空间

    oracle 怎么查看用户对应的表空间? 查询用户: 查看数据库里面所有用户,前提是你是有 dba 权限的帐号,如 sys,system: select * from dba_users; 查看你能管 ...