低代码3之撤销与重做操作以及快捷键ctrl+z与ctrl+y
低代码3之撤销与重做操作以及快捷键ctrl+z与ctrl+y
·
目录
- 安装依赖
npm i mitt
- 功能:
src / package / useCommand.js
// 菜单拖拽功能
import { events } from "./events"
export function useMenuDragger (containerRef,data){
let currentComponent = null // 当前拖拽的组件
const dragstart = (e,component)=>{
// dragenter 进入元素中,添加一个移动标识
// dragover 在目标元素经过 必须要组织默认事件 否则不能触发drop
// dragleave 离开元素的时候 需要增加一个禁用的标识
// drop 松手的时候 根据拖拽的组件 添加一个组件
containerRef.value.addEventListener('dragenter',dragenter)
containerRef.value.addEventListener('dragover',dragover)
containerRef.value.addEventListener('dragleave',dragleave)
containerRef.value.addEventListener('drop',drop)
currentComponent = component;
events.emit('start'); // 拖拽前
}
// 拖拽结束 去掉事件 + 清空当前组件
const dragend = (e)=>{
containerRef.value.removeEventListener('dragenter',dragenter)
containerRef.value.removeEventListener('dragover',dragover)
containerRef.value.removeEventListener('dragleave',dragleave)
containerRef.value.removeEventListener('drop',drop)
currentComponent = null;
events.emit('end'); // 拖拽后
}
const dragenter = (e)=>{
e.dataTransfer.dropEffect = 'move'; // 移动标识
}
const dragover = (e)=>{
e.preventDefault(); // 阻止默认行为
}
const dragleave = (e)=>{
e.dataTransfer.dropEffect = 'none'; // 离开 设置禁用
}
const drop = (e)=>{
let blocks = data.value.blocks; // 内部渲染的组件
// 更新data的数据 触发更新app传递进来的v-model数据
data.value = {
...data.value,
blocks:[
...blocks,
// 拿到拖拽入容器的组件 位置 与 key值
{ top:e.offsetY, left:e.offsetX ,zIndex:1,
key:currentComponent.key,
alignCenter:true // 松手时居中 需要在editor-blick.jsx之中配置
}
]
}
currentComponent = null
}
return {
dragstart,
dragend
}
}
src / package / events.js
import mitt from "mitt";
export const events = mitt(); // 导出一个发布订阅的对象 (类型vue2的bus)
src / package / useCommand.js
import deepcopy from "deepcopy";
import { onUnmounted } from "vue";
import { events } from "./events";
export function useCommand(data) {
const state = {
// 前进和后退都需要指针
current: -1, // 前进后退的索引
queue: [], // 存放所有的操作指令
commands: {}, // 制作命令和执行功能一个映射表 undo: ()=>{} ; redo: ()=>{}
commandArray: [], // 存放所有的命令
destoryArray: [], // 销毁的命令
};
const regeister = (command) => {
state.commandArray.push(command);
state.commands[command.name] = () => {
// 命令名字 映射 执行函数
const { redo, undo } = command.execute();
redo();
if (!command.pushQueue) {
// 判断是否需要放入队列之中 不需要直接return
return;
}
let { queue, current } = state;
// 如果 放入了组件1 组件2 点击撤回 放入组件3 => 最终结果为 组件1 组件3
if (queue.length > 0) {
queue = queue.slice(0, current + 1); // 以当前的位置 截取最新的数据(可能撤销了几个),以当前最新的current来计算值
state.queue = queue;
}
queue.push({
// 保存指令的前进和后退
redo,
undo,
});
state.current = current + 1;
};
};
// 注册我们需要的命令
// 重做命令
regeister({
name: "redo",
keyboard: "ctrl+y",
execute() {
return {
redo() {
let item = state.queue[state.current + 1]; // 找到当前的下一步操作
if (item) {
// 若是有值 则撤销
item.redo && item.redo();
state.current++;
}
},
};
},
});
// 撤销命令
regeister({
name: "undo",
keyboard: "ctrl+z",
execute() {
return {
redo() {
if (state.current == -1) return; // 若是没有 则退出
let item = state.queue[state.current]; // 找到当前的上一步操作
if (item) {
// 若是有值 则撤销
item.undo && item.undo();
state.current--;
}
},
};
},
});
// 拖拽命令 如果希望将操作放到队列中 可以增加一个属性 标识等会操作的要放到队列中
regeister({
name: "drag",
pushQueue: true,
init() {
// 初始化操作 默认就执行
this.before = null; // 操作前的数据
// 监控拖拽开始事件,保存当前状态
const start = () => (this.before = deepcopy(data.value.blocks));
// 拖拽后 需要触发的对应指令
const end = () => state.commands.drag();
events.on("start", start);
events.on("end", end);
return () => {
// 卸载函数
events.off("start", start);
events.off("end", end);
};
},
execute() {
let before = this.before; // 之前的
let after = data.value.blocks; // 最新的
return {
redo() {
// 更新最新的值 默认一松手的时候 把当前的事情处理了
data.value = { ...data.value, blocks: after };
},
undo() {
// 撤销 往前面退一个操作
data.value = { ...data.value, blocks: before };
},
};
},
});
const keyboardEvent = (() => {
const init = () => {
// 初始化事件
const keyCodes = {
90: "z",
89: "y",
};
const onKeydown = (e) => {
const { ctrlKey, keyCode } = e;
let keyStrings = []; // ctrl+z 或者 ctrl+y
if (ctrlKey) keyStrings.push("ctrl");
keyStrings.push(keyCodes[keyCode]);
keyStrings = keyStrings.join("+");
state.commandArray.forEach(({keyboard, name}) => {
if (!keyboard) return;
if (keyboard === keyStrings) {
state.commands[name]();
e.preventDefault(); // 阻止默认行为
}
});
};
window.addEventListener("keydown", onKeydown);
return () => {
// 销毁事件
window.removeEventListener("keydown", onKeydown);
};
};
return init;
})();
// 查看是否有拖拽后的效果 有立即执行更新操作 并且存放当前数据(用于销毁撤销)
(() => {
// 监听键盘事件 ctrl + z 实现撤销
state.destoryArray.push(keyboardEvent());
// 查看是否有拖拽后的效果 有立即执行更新操作 并且存放当前数据(用于销毁撤销)
state.commandArray.forEach(
(command) => command.init && state.destoryArray.push(command.init())
);
})();
onUnmounted(() => {
// 清理绑定的事件
state.destoryArray.forEach((fn) => fn && fn());
});
return state;
}
src / package / editor.jsx
import { computed, defineComponent, inject, ref } from "vue";
import "./editor.scss";
import EditorBlock from "./editor-block";
import deepcopy from "deepcopy";
import { useMenuDragger } from "./useMenuDragger";
import { useFocus } from "./useFocus";
import { useBlockDragger } from "./useBlockDragger";
import { useCommand } from "./useCommand";
export default defineComponent({
props: {
modelValue: {
type: Object,
},
},
emits: ["update:modelValue"], // 1:菜单拖拽功能-03:触发事件 更新app的数据 set之中更新
setup(props, ctx) {
// console.log('props',props.modelValue);
const data = computed({
get() {
return props.modelValue;
},
set(newValue) {
ctx.emit("update:modelValue", deepcopy(newValue));
},
});
const contentStyle = computed(() => ({
width: data.value.container.width + "px",
height: data.value.container.height + "px",
}));
const config = inject("config");
// 1:菜单拖拽功能-02:实现h5的拖拽放入组件容器形成被拖拽的组件 useMenuDragger实现左侧菜单拖拽功能
const containerRef = ref(null);
const { dragstart, dragend } = useMenuDragger(containerRef, data);
// 2:容器内获取焦点功能-01:点击容器时候聚焦与按住shift时候支持多个聚焦;选中后拖拽
const { blockMousedown, containerMousedown, focusData,lastSelectBlock } = useFocus(
data,
(e) => {
// 3:获取焦点后 进行拖拽-02
mousedown(e, focusData);
}
);
// 3:实现组件拖拽-01:
const { mousedown,markLine } = useBlockDragger(focusData,lastSelectBlock,data);
// 4:每一次操作的记录 撤销与重做功能
const {commands} = useCommand(data)
const buttons = [
{lable:'撤销',icon:'',handler:()=>commands.undo()},
{lable:'重做',icon:'',handler:()=>commands.redo()}
]
return () => (
<div class="editor">
<div class="editor-left">
{/** 根据config的注册列表 渲染出左侧的物料区域、:1:菜单拖拽功能-01:实现h5的拖拽-draggable */}
{config.componetsList.map((component) => (
<div
class="editor-left-item"
draggable
onDragstart={(e) => dragstart(e, component)}
onDragend={dragend}
>
<span class="editor-left-item-label">{component.label}</span>
<div>{component.preview()}</div>
</div>
))}
</div>
<div className="editor-center">
<div class="editor-top">
{
buttons.map((btn,idx)=>{
return <div class={'editor-top-btn'} onClick={btn.handler}>
<i class={btn.icon}></i>
<span>{btn.lable}</span>
</div>
})
}
</div>
<div class="editor-content">
{/* 负责尝试滚动条 */}
<div class="editor-content-canvas">
{/* 产生内容区域 */}
<div
class="editor-content-canvas_content"
style={contentStyle.value}
ref={containerRef}
onMousedown={containerMousedown}
>
{data.value.blocks.map((block,index) => (
<EditorBlock
class={block.focus ? "editoe-blick-focus" : ""}
block={block}
onMousedown={(e) => blockMousedown(e, block,index)}
></EditorBlock>
))}
{/**辅助线 */}
{ markLine.x !== null && <div class='line-x' style={{left:markLine.x + 'px'}}></div>}
{ markLine.y !== null && <div class='line-y' style={{top:markLine.y + 'px'}}></div>}
</div>
</div>
</div>
</div>
<div class="editor-right">right</div>
</div>
);
},
});
src / package / editor.scss
.editor {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
.editor-left ,.editor-right{
width: 270px;
background: yellow;
height: 100%;
}
.editor-left {
.editor-left-item {
position: relative;
width: 250px;
margin: 20px auto;
display: flex;
justify-content: center;
align-items: center;
background: #ccc;
padding: 20px;
box-sizing: border-box;
cursor: move;
user-select: none; // 无法操作
min-height: 100px;
.editor-left-item-label {
position: absolute;
left: 0;
top: 0;
background: rgb(96, 205, 224);
color: #fff;
padding: 4px;
}
// 设置 item 项目不可点击等
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #ccc;
opacity: 0.2;
}
}
}
.editor-center {
width: calc(100% - 270px - 270px - 20px);
padding: 0 10px;
background: orange;
height: 100%;
}
.editor-top {
height: 80px;
background: pink;
display: flex;
justify-content: center;
align-items: center;
.editor-top-btn {
width: 60px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0,0,0,.3);
color: #fff;
user-select: none;
cursor: pointer;
margin-left: 3px;
}
}
.editor-content {
height: calc(100% - 80px);
background: orange;
.editor-content-canvas {
overflow: scroll;
height: 100%;
}
.editor-content-canvas_content {
position: relative;
margin: 20px auto;
// height: calc(100% - 40px);
// width: 550px;
// height: 550px;
background: #ccc;
}
}
}
.editor-block {
position: absolute;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.editoe-blick-focus {
&::after {
border: 2px dashed red;
}
}
// 移动的left值
.line-x {
position: absolute;
top: 0;
bottom: 0;
border-left:1px dashed red;
}
// 移动的top值
.line-y {
position: absolute;
left: 0;
right: 0;
border-top:1px dashed red;
}
更多推荐


所有评论(0)