feat: add onChanged callback, Delete key support, drag offset fix

- Add onChanged(callback) API for tracking page changes
- Support Delete/Backspace key to delete selected component
- Fix drag offset issue when moving components
- Add unsaved changes tracking in editor demo
- Update README documentation
This commit is contained in:
wtz
2026-03-29 13:01:53 +08:00
parent e4fe6aac1d
commit 1d58d40185
3 changed files with 272 additions and 161 deletions

View File

@@ -9,9 +9,11 @@ IoT 可视化编辑器库 - 基于网格布局的拖拽式页面构建器
- 📱 固定手机比例画布375×812
- 📐 网格布局,单元格 1:1 正方形
- 🖱️ 拖拽添加、移动组件
- ⌨️ Delete/Backspace键删除选中组件
- ⚙️ 属性面板,实时预览
- 💾 JSONB 格式保存/加载
- 🎨 自定义组件支持
- 🔔 变化回调通知
## 项目结构
@@ -21,10 +23,10 @@ IoT 可视化编辑器库 - 基于网格布局的拖拽式页面构建器
│ ├── webbuilder.js # 主文件
│ └── types/ # 内部模块
├── editor.html # 示例编辑器
├── js/editor/editor.js # 示例编辑器逻辑
├── css/editor.css # 示例编辑器样式
├── README.md # 中文文档
└── README_EN.md # English
├── js/editor/editor.js # 示例编辑器逻辑
├── css/editor.css # 示例编辑器样式
├── README.md # 中文文档
└── README_EN.md # English
```
> **注意**: `editor.html` 和 `js/editor/` 是示例代码,展示如何使用本库。
@@ -112,11 +114,29 @@ webbuilder.define(name, definition)
defaultColumnSpan: 6, // 默认跨列数
defaultRowSpan: 4, // 默认跨行数
defaultProps: { ... }, // 默认属性
traits: [ ... ], // 属性定义(用于属性面板)
traits: [ ... ], // 属性定义(用于属性面板)
render: function(props) { ... } // 渲染函数,返回 DOM 元素
}
```
### 注册变化回调
```javascript
webbuilder.onChanged(function(action, data) {
// action: 'add' | 'delete' | 'move' | 'update' | 'load' | 'clear'
// data: 相关数据组件实例、组件ID等
});
```
| action | 说明 | data |
|--------|------|------|
| add | 添加组件 | 组件实例对象 |
| delete | 删除组件 | { id: 组件ID } |
| move | 移动组件 | 组件实例对象 |
| update | 属性面板修改属性 | 组件实例对象 |
| load | 加载JSONB | { count: 组件数量 } |
| clear | 清空画布 | null |
### 导出 JSONB
```javascript
@@ -176,7 +196,7 @@ var components = webbuilder.renderToDiv(container);
{
element: HTMLElement, // 组件 DOM 元素
wrapper: HTMLElement, // 包装容器
props: { ... }, // 组件属性(深拷贝)
props: { ... }, // 组件属性(深拷贝)
id: 'gauge-1',
type: 'gauge'
}

View File

@@ -3,6 +3,9 @@
* 仅支持手机端布局
*/
// 未保存标记
var isDirty = false;
// 初始化编辑器
function initEditor() {
// 初始化 webbuilder
@@ -13,6 +16,11 @@ function initEditor() {
// 绑定事件
bindEvents();
// 注册变化回调
webbuilder.onChanged(function(action, data) {
isDirty = true;
});
}
/**
@@ -195,10 +203,14 @@ function bindEvents() {
var jsonb = webbuilder.toJSONB();
var jsonStr = JSON.stringify(jsonb, null, 2);
downloadFile('canvas-config.json', jsonStr, 'application/json');
isDirty = false;
});
// 加载 JSONB
document.getElementById('btn-load').addEventListener('click', function() {
if (isDirty && !confirm('当前有未保存的更改,确定要加载新文件吗?')) {
return;
}
document.getElementById('file-input').click();
});
@@ -211,6 +223,7 @@ function bindEvents() {
try {
var jsonb = JSON.parse(e.target.result);
webbuilder.fromJSONB(jsonb);
isDirty = false;
} catch (err) {
alert('加载失败:' + err.message);
}
@@ -260,8 +273,12 @@ function bindEvents() {
// 清空画布
document.getElementById('btn-clear').addEventListener('click', function() {
if (isDirty && !confirm('当前有未保存的更改,确定要清空画布吗?')) {
return;
}
if (confirm('确定要清空画布吗?')) {
webbuilder.clear();
isDirty = true;
}
});
@@ -283,6 +300,14 @@ function bindEvents() {
var code = document.getElementById('preview-code').textContent;
downloadFile('page.html', code, 'text/html');
});
// 页面离开提示
window.addEventListener('beforeunload', function(e) {
if (isDirty) {
e.preventDefault();
e.returnValue = '';
}
});
}
/**

View File

@@ -34,9 +34,15 @@ var webbuilder = {
draggingComponent: null,
draggingType: null,
// 拖拽时鼠标相对于组件的偏移
dragOffset: { x: 0, y: 0 },
// 唯一ID计数器
idCounter: 0,
// 变化回调函数
onChangedCallback: null,
/**
* 计算单元格大小和行数
*/
@@ -87,6 +93,9 @@ var webbuilder = {
// 初始化工具箱
this._initToolbox();
// 初始化键盘事件
this._initKeyboardEvents();
return this;
},
@@ -180,6 +189,41 @@ var webbuilder = {
this.toolboxEl.appendChild(this.toolboxListEl);
},
/**
* 初始化键盘事件
*/
_initKeyboardEvents: function() {
var self = this;
document.addEventListener('keydown', function(e) {
// Delete键删除选中的组件
if (e.key === 'Delete' || e.key === 'Backspace') {
if (self.selectedComponent) {
e.preventDefault();
self._deleteComponent(self.selectedComponent);
}
}
});
},
/**
* 注册变化回调
* @param {function} callback - 回调函数签名callback(action, data)
* action: 'add' | 'delete' | 'move' | 'update' | 'load' | 'clear'
* data: 相关数据组件实例、组件ID等
*/
onChanged: function(callback) {
this.onChangedCallback = callback;
},
/**
* 触发变化回调
*/
_notifyChanged: function(action, data) {
if (typeof this.onChangedCallback === 'function') {
this.onChangedCallback(action, data);
}
},
/**
* 计算网格位置
*/
@@ -187,9 +231,9 @@ var webbuilder = {
var rect = this.canvasEl.getBoundingClientRect();
var config = this.phoneConfig;
// 计算鼠标相对于画布的位置
var x = e.clientX - rect.left - config.gap;
var y = e.clientY - rect.top - config.gap;
// 计算鼠标相对于画布的位置,减去偏移量
var x = e.clientX - rect.left - config.gap - this.dragOffset.x;
var y = e.clientY - rect.top - config.gap - this.dragOffset.y;
// 计算列和行
var col = Math.floor(x / (this.cellSize + config.gap)) + 1;
@@ -238,6 +282,9 @@ var webbuilder = {
// 选中新添加的组件
this._selectComponent(instance.id);
// 触发回调
this._notifyChanged('add', instance);
},
/**
@@ -285,6 +332,10 @@ var webbuilder = {
e.stopPropagation();
self.draggingComponent = instance.id;
el.style.opacity = '0.5';
// 计算鼠标相对于组件的偏移
var rect = el.getBoundingClientRect();
self.dragOffset.x = e.clientX - rect.left;
self.dragOffset.y = e.clientY - rect.top;
});
el.addEventListener('dragend', function(e) {
el.style.opacity = '1';
@@ -397,6 +448,9 @@ var webbuilder = {
if (this.selectedComponent === componentId) {
this._showPropertyPanel(instance);
}
// 触发回调
this._notifyChanged('move', instance);
},
/**
@@ -486,7 +540,7 @@ var webbuilder = {
if (prop) {
// 更新网格位置
var value = parseInt(input.value) || 1;
var value = parseInt(input.value, 10) || 1;
if (prop === 'startCol') {
var currentRow = Math.floor(instance.grid.startCell / columns);
instance.grid.startCell = currentRow * columns + (value - 1);
@@ -530,6 +584,9 @@ var webbuilder = {
}
}
}
// 触发属性更新回调
self._notifyChanged('update', instance);
});
});
@@ -568,6 +625,9 @@ var webbuilder = {
// 取消选中
this._deselectAll();
// 触发回调
this._notifyChanged('delete', { id: componentId });
},
/**
@@ -712,11 +772,14 @@ var webbuilder = {
this.componentInstances.forEach(function(inst) {
var match = inst.id.match(/-(\d+)$/);
if (match) {
maxId = Math.max(maxId, parseInt(match[1]));
maxId = Math.max(maxId, parseInt(match[1], 10));
}
});
this.idCounter = maxId;
}
// 触发回调
this._notifyChanged('load', { count: this.componentInstances.length });
},
/**
@@ -749,5 +812,8 @@ var webbuilder = {
this.componentInstances = [];
this.selectedComponent = null;
this.propertyPanelEl.innerHTML = '';
// 触发回调
this._notifyChanged('clear', null);
}
};