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:
20
README.md
20
README.md
@@ -9,9 +9,11 @@ IoT 可视化编辑器库 - 基于网格布局的拖拽式页面构建器
|
|||||||
- 📱 固定手机比例画布(375×812)
|
- 📱 固定手机比例画布(375×812)
|
||||||
- 📐 网格布局,单元格 1:1 正方形
|
- 📐 网格布局,单元格 1:1 正方形
|
||||||
- 🖱️ 拖拽添加、移动组件
|
- 🖱️ 拖拽添加、移动组件
|
||||||
|
- ⌨️ Delete/Backspace键删除选中组件
|
||||||
- ⚙️ 属性面板,实时预览
|
- ⚙️ 属性面板,实时预览
|
||||||
- 💾 JSONB 格式保存/加载
|
- 💾 JSONB 格式保存/加载
|
||||||
- 🎨 自定义组件支持
|
- 🎨 自定义组件支持
|
||||||
|
- 🔔 变化回调通知
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -117,6 +119,24 @@ webbuilder.define(name, definition)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 注册变化回调
|
||||||
|
|
||||||
|
```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
|
### 导出 JSONB
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
* 仅支持手机端布局
|
* 仅支持手机端布局
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 未保存标记
|
||||||
|
var isDirty = false;
|
||||||
|
|
||||||
// 初始化编辑器
|
// 初始化编辑器
|
||||||
function initEditor() {
|
function initEditor() {
|
||||||
// 初始化 webbuilder
|
// 初始化 webbuilder
|
||||||
@@ -13,6 +16,11 @@ function initEditor() {
|
|||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
|
// 注册变化回调
|
||||||
|
webbuilder.onChanged(function(action, data) {
|
||||||
|
isDirty = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,10 +203,14 @@ function bindEvents() {
|
|||||||
var jsonb = webbuilder.toJSONB();
|
var jsonb = webbuilder.toJSONB();
|
||||||
var jsonStr = JSON.stringify(jsonb, null, 2);
|
var jsonStr = JSON.stringify(jsonb, null, 2);
|
||||||
downloadFile('canvas-config.json', jsonStr, 'application/json');
|
downloadFile('canvas-config.json', jsonStr, 'application/json');
|
||||||
|
isDirty = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载 JSONB
|
// 加载 JSONB
|
||||||
document.getElementById('btn-load').addEventListener('click', function() {
|
document.getElementById('btn-load').addEventListener('click', function() {
|
||||||
|
if (isDirty && !confirm('当前有未保存的更改,确定要加载新文件吗?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById('file-input').click();
|
document.getElementById('file-input').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,6 +223,7 @@ function bindEvents() {
|
|||||||
try {
|
try {
|
||||||
var jsonb = JSON.parse(e.target.result);
|
var jsonb = JSON.parse(e.target.result);
|
||||||
webbuilder.fromJSONB(jsonb);
|
webbuilder.fromJSONB(jsonb);
|
||||||
|
isDirty = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('加载失败:' + err.message);
|
alert('加载失败:' + err.message);
|
||||||
}
|
}
|
||||||
@@ -260,8 +273,12 @@ function bindEvents() {
|
|||||||
|
|
||||||
// 清空画布
|
// 清空画布
|
||||||
document.getElementById('btn-clear').addEventListener('click', function() {
|
document.getElementById('btn-clear').addEventListener('click', function() {
|
||||||
|
if (isDirty && !confirm('当前有未保存的更改,确定要清空画布吗?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (confirm('确定要清空画布吗?')) {
|
if (confirm('确定要清空画布吗?')) {
|
||||||
webbuilder.clear();
|
webbuilder.clear();
|
||||||
|
isDirty = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,6 +300,14 @@ function bindEvents() {
|
|||||||
var code = document.getElementById('preview-code').textContent;
|
var code = document.getElementById('preview-code').textContent;
|
||||||
downloadFile('page.html', code, 'text/html');
|
downloadFile('page.html', code, 'text/html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 页面离开提示
|
||||||
|
window.addEventListener('beforeunload', function(e) {
|
||||||
|
if (isDirty) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,9 +34,15 @@ var webbuilder = {
|
|||||||
draggingComponent: null,
|
draggingComponent: null,
|
||||||
draggingType: null,
|
draggingType: null,
|
||||||
|
|
||||||
|
// 拖拽时鼠标相对于组件的偏移
|
||||||
|
dragOffset: { x: 0, y: 0 },
|
||||||
|
|
||||||
// 唯一ID计数器
|
// 唯一ID计数器
|
||||||
idCounter: 0,
|
idCounter: 0,
|
||||||
|
|
||||||
|
// 变化回调函数
|
||||||
|
onChangedCallback: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算单元格大小和行数
|
* 计算单元格大小和行数
|
||||||
*/
|
*/
|
||||||
@@ -87,6 +93,9 @@ var webbuilder = {
|
|||||||
// 初始化工具箱
|
// 初始化工具箱
|
||||||
this._initToolbox();
|
this._initToolbox();
|
||||||
|
|
||||||
|
// 初始化键盘事件
|
||||||
|
this._initKeyboardEvents();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -180,6 +189,41 @@ var webbuilder = {
|
|||||||
this.toolboxEl.appendChild(this.toolboxListEl);
|
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 rect = this.canvasEl.getBoundingClientRect();
|
||||||
var config = this.phoneConfig;
|
var config = this.phoneConfig;
|
||||||
|
|
||||||
// 计算鼠标相对于画布的位置
|
// 计算鼠标相对于画布的位置,减去偏移量
|
||||||
var x = e.clientX - rect.left - config.gap;
|
var x = e.clientX - rect.left - config.gap - this.dragOffset.x;
|
||||||
var y = e.clientY - rect.top - config.gap;
|
var y = e.clientY - rect.top - config.gap - this.dragOffset.y;
|
||||||
|
|
||||||
// 计算列和行
|
// 计算列和行
|
||||||
var col = Math.floor(x / (this.cellSize + config.gap)) + 1;
|
var col = Math.floor(x / (this.cellSize + config.gap)) + 1;
|
||||||
@@ -238,6 +282,9 @@ var webbuilder = {
|
|||||||
|
|
||||||
// 选中新添加的组件
|
// 选中新添加的组件
|
||||||
this._selectComponent(instance.id);
|
this._selectComponent(instance.id);
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('add', instance);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -285,6 +332,10 @@ var webbuilder = {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
self.draggingComponent = instance.id;
|
self.draggingComponent = instance.id;
|
||||||
el.style.opacity = '0.5';
|
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.addEventListener('dragend', function(e) {
|
||||||
el.style.opacity = '1';
|
el.style.opacity = '1';
|
||||||
@@ -397,6 +448,9 @@ var webbuilder = {
|
|||||||
if (this.selectedComponent === componentId) {
|
if (this.selectedComponent === componentId) {
|
||||||
this._showPropertyPanel(instance);
|
this._showPropertyPanel(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('move', instance);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -486,7 +540,7 @@ var webbuilder = {
|
|||||||
|
|
||||||
if (prop) {
|
if (prop) {
|
||||||
// 更新网格位置
|
// 更新网格位置
|
||||||
var value = parseInt(input.value) || 1;
|
var value = parseInt(input.value, 10) || 1;
|
||||||
if (prop === 'startCol') {
|
if (prop === 'startCol') {
|
||||||
var currentRow = Math.floor(instance.grid.startCell / columns);
|
var currentRow = Math.floor(instance.grid.startCell / columns);
|
||||||
instance.grid.startCell = currentRow * columns + (value - 1);
|
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._deselectAll();
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('delete', { id: componentId });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -712,11 +772,14 @@ var webbuilder = {
|
|||||||
this.componentInstances.forEach(function(inst) {
|
this.componentInstances.forEach(function(inst) {
|
||||||
var match = inst.id.match(/-(\d+)$/);
|
var match = inst.id.match(/-(\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
maxId = Math.max(maxId, parseInt(match[1]));
|
maxId = Math.max(maxId, parseInt(match[1], 10));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.idCounter = maxId;
|
this.idCounter = maxId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('load', { count: this.componentInstances.length });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -749,5 +812,8 @@ var webbuilder = {
|
|||||||
this.componentInstances = [];
|
this.componentInstances = [];
|
||||||
this.selectedComponent = null;
|
this.selectedComponent = null;
|
||||||
this.propertyPanelEl.innerHTML = '';
|
this.propertyPanelEl.innerHTML = '';
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('clear', null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user