可视化编辑器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. 在VMD上可视化hdf5格式的分子轨迹文件

    技术背景 在处理分子动力学模拟的数据时,不可避免的会遇到众多的大轨迹文件.因此以什么样的格式来存储这些庞大的轨迹数据,也是一个在分子动力学模拟软件设计初期就应该妥善考虑的问题.现有的比较常见的方式,大 ...

  2. 封装Fraction-分数类(C++)

    Fraction 分数类 默认假分数,可自行修改 由于concept的原因 template <typename T> concept is_float_v = std::is_float ...

  3. Gulp介绍及安装使用教程

    一.简介 gulp是前端开发过程中对代码进行构建的工具,是自动化项目的构建利器,不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成,使用她,我们不仅可以很愉快的编写代码 ...

  4. {版本发布公告}HMS Core 6.6.0来啦

    分析服务 ◆ 留存分析支持¬将流失用户存为受众,开发者通过对流失人群的分层以及多维分析,在制定相关用户召回策略时将更有针对性: ◆ 原"受众分析"更名为"人群洞察&quo ...

  5. 网安等保-Linux服务器之最新Ubuntu-22.04-LTS系统内核优化与安全加固配置脚本使用分享

    关注「WeiyiGeek」公众号 设为「特别关注」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 0x00 前言 ...

  6. Android 自动取色并设置沉浸式状态栏

    Android 自动取色并设置沉浸式状态栏 - Stars-One的杂货小窝 最近在进行产品的优化,也是研究了下沉浸式状态栏的实现方法及自动取色,记录一下笔记 设置沉浸式状态栏 1.添加依赖 这里,是 ...

  7. iOS 集成WebRTC相关知识点总结

    前言 本文主要是整理了使用WebRTC做音视频通讯时的各知识点及问题点.有理解不足和不到位的地方也欢迎指正. 对于你感兴趣的部分可以选择性观看. WebRTC的初始化 在使用WebRTC的库之前,需要 ...

  8. C++工厂方法模式讲解和代码示例

    在C++中使用模式 使用示例: 工厂方法模式在 C++ 代码中得到了广泛使用. 当你需要在代码中提供高层次的灵活性时, 该模式会非常实用. 识别方法: 工厂方法可通过构建方法来识别, 它会创建具体类的 ...

  9. 如何不编写 YAML 管理 Kubernetes 应用?

    Kubernetes 将自身边界内的事物都抽象为资源.其中的主要部分,是以 Deployment.StatefulSet 为代表的 workload 工作负载控制器,其他各类资源都围绕这些主要的资源工 ...

  10. 图解Kubernetes的Pod核心资源-来白嫖啊

    目录 一.Pod定义 二.Pod入门yaml描述文件 三.共享NetworkNamespace 四.共享PID 五.容器生命周期 六.初始化容器 6.1.简介 6.2.与普通容器的区别 6.3.实验 ...