feat: add resize handles for component size editing
Add resize handles to allow direct size editing of components. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -189,6 +189,8 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.webbuilder-component:hover {
|
.webbuilder-component:hover {
|
||||||
@@ -200,6 +202,67 @@ body {
|
|||||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 尺寸控制柄容器 */
|
||||||
|
.resize-handles-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 尺寸控制柄 */
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #1890ff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 11;
|
||||||
|
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
background: #40a9ff;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-nw {
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-n {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-ne {
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-w {
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-e {
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-sw {
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-s {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-se {
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
/* 属性面板 */
|
/* 属性面板 */
|
||||||
.webbuilder-property-panel {
|
.webbuilder-property-panel {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ var webbuilder = {
|
|||||||
// 变化回调函数
|
// 变化回调函数
|
||||||
onChangedCallback: null,
|
onChangedCallback: null,
|
||||||
|
|
||||||
|
// 尺寸控制柄相关状态
|
||||||
|
resizeHandles: null,
|
||||||
|
isResizing: false,
|
||||||
|
resizeDirection: null,
|
||||||
|
resizeStartPos: { x: 0, y: 0 },
|
||||||
|
resizeStartSize: { colSpan: 0, rowSpan: 0 },
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算单元格大小和行数
|
* 计算单元格大小和行数
|
||||||
*/
|
*/
|
||||||
@@ -96,6 +103,9 @@ var webbuilder = {
|
|||||||
// 初始化键盘事件
|
// 初始化键盘事件
|
||||||
this._initKeyboardEvents();
|
this._initKeyboardEvents();
|
||||||
|
|
||||||
|
// 初始化尺寸控制柄事件
|
||||||
|
this._initResizeEvents();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -134,34 +144,34 @@ var webbuilder = {
|
|||||||
this.canvasEl.style.width = config.width + 'px';
|
this.canvasEl.style.width = config.width + 'px';
|
||||||
this.canvasEl.style.height = config.height + 'px';
|
this.canvasEl.style.height = config.height + 'px';
|
||||||
this.canvasEl.style.display = 'grid';
|
this.canvasEl.style.display = 'grid';
|
||||||
this.canvasEl.style.gridTemplateColumns = 'repeat(' + config.columns + ', ' + this.cellSize + 'px)';
|
this.canvasEl.style.gridTemplateColumns = `repeat(${config.columns}, ${this.cellSize}px)`;
|
||||||
this.canvasEl.style.gridAutoRows = this.cellSize + 'px';
|
this.canvasEl.style.gridAutoRows = `${this.cellSize}px`;
|
||||||
this.canvasEl.style.gap = config.gap + 'px';
|
this.canvasEl.style.gap = `${config.gap}px`;
|
||||||
this.canvasEl.style.background = config.background;
|
this.canvasEl.style.background = config.background;
|
||||||
this.canvasEl.style.position = 'relative';
|
this.canvasEl.style.position = 'relative';
|
||||||
this.canvasEl.style.padding = config.gap + 'px';
|
this.canvasEl.style.padding = `${config.gap}px`;
|
||||||
this.canvasEl.style.boxSizing = 'border-box';
|
this.canvasEl.style.boxSizing = 'border-box';
|
||||||
this.canvasWrapperEl.appendChild(this.canvasEl);
|
this.canvasWrapperEl.appendChild(this.canvasEl);
|
||||||
|
|
||||||
// 画布点击取消选中
|
// 画布点击取消选中
|
||||||
var self = this;
|
var self = this;
|
||||||
this.canvasEl.addEventListener('click', function(e) {
|
this.canvasEl.addEventListener('click', (e) => {
|
||||||
if (e.target === self.canvasEl) {
|
if (e.target === self.canvasEl) {
|
||||||
self._deselectAll();
|
self._deselectAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 拖放事件
|
// 拖放事件
|
||||||
this.canvasEl.addEventListener('dragover', function(e) {
|
this.canvasEl.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
self.canvasEl.classList.add('dropping');
|
self.canvasEl.classList.add('dropping');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvasEl.addEventListener('dragleave', function(e) {
|
this.canvasEl.addEventListener('dragleave', (e) => {
|
||||||
self.canvasEl.classList.remove('dropping');
|
self.canvasEl.classList.remove('dropping');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvasEl.addEventListener('drop', function(e) {
|
this.canvasEl.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
self.canvasEl.classList.remove('dropping');
|
self.canvasEl.classList.remove('dropping');
|
||||||
|
|
||||||
@@ -194,7 +204,18 @@ var webbuilder = {
|
|||||||
*/
|
*/
|
||||||
_initKeyboardEvents: function() {
|
_initKeyboardEvents: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// 如果焦点在输入框中,不处理删除操作
|
||||||
|
var target = e.target;
|
||||||
|
var isInputElement = target.tagName === 'INPUT' ||
|
||||||
|
target.tagName === 'TEXTAREA' ||
|
||||||
|
target.tagName === 'SELECT' ||
|
||||||
|
target.isContentEditable;
|
||||||
|
|
||||||
|
if (isInputElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Delete键删除选中的组件
|
// Delete键删除选中的组件
|
||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
if (self.selectedComponent) {
|
if (self.selectedComponent) {
|
||||||
@@ -205,6 +226,29 @@ var webbuilder = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化尺寸控制柄事件
|
||||||
|
*/
|
||||||
|
_initResizeEvents: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// 鼠标移动事件
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (self.isResizing && self.selectedComponent) {
|
||||||
|
e.preventDefault();
|
||||||
|
self._handleResizeMove(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 鼠标释放事件
|
||||||
|
document.addEventListener('mouseup', (e) => {
|
||||||
|
if (self.isResizing) {
|
||||||
|
e.preventDefault();
|
||||||
|
self._handleResizeEnd(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册变化回调
|
* 注册变化回调
|
||||||
* @param {function} callback - 回调函数,签名:callback(action, data)
|
* @param {function} callback - 回调函数,签名:callback(action, data)
|
||||||
@@ -308,10 +352,10 @@ var webbuilder = {
|
|||||||
el.className = 'webbuilder-component';
|
el.className = 'webbuilder-component';
|
||||||
el.setAttribute('data-id', instance.id);
|
el.setAttribute('data-id', instance.id);
|
||||||
el.setAttribute('data-type', instance.type);
|
el.setAttribute('data-type', instance.type);
|
||||||
el.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
el.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
|
||||||
el.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
|
el.style.gridRow = `${startRow + 1} / span ${rowSpan}`;
|
||||||
el.style.position = 'relative';
|
el.style.position = 'relative';
|
||||||
el.style.overflow = 'hidden';
|
el.style.overflow = 'visible';
|
||||||
|
|
||||||
// 渲染组件内容 - render 函数返回 HTML 元素对象
|
// 渲染组件内容 - render 函数返回 HTML 元素对象
|
||||||
if (typeDef.render) {
|
if (typeDef.render) {
|
||||||
@@ -325,10 +369,95 @@ var webbuilder = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使组件可拖拽
|
// 创建尺寸控制柄容器
|
||||||
|
var handlesContainer = document.createElement('div');
|
||||||
|
handlesContainer.className = 'resize-handles-container';
|
||||||
|
handlesContainer.style.display = 'none';
|
||||||
|
handlesContainer.style.position = 'absolute';
|
||||||
|
handlesContainer.style.top = '0';
|
||||||
|
handlesContainer.style.left = '0';
|
||||||
|
handlesContainer.style.width = '100%';
|
||||||
|
handlesContainer.style.height = '100%';
|
||||||
|
handlesContainer.style.pointerEvents = 'none';
|
||||||
|
el.appendChild(handlesContainer);
|
||||||
|
|
||||||
|
// 创建8个控制柄
|
||||||
|
var directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
directions.forEach((direction) => {
|
||||||
|
var handle = document.createElement('div');
|
||||||
|
handle.className = `resize-handle resize-handle-${direction}`;
|
||||||
|
handle.setAttribute('data-direction', direction);
|
||||||
|
handle.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
|
// 控制柄位置
|
||||||
|
switch(direction) {
|
||||||
|
case 'nw':
|
||||||
|
handle.style.top = '-4px';
|
||||||
|
handle.style.left = '-4px';
|
||||||
|
handle.style.cursor = 'nwse-resize';
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
handle.style.top = '-4px';
|
||||||
|
handle.style.left = '50%';
|
||||||
|
handle.style.transform = 'translateX(-50%)';
|
||||||
|
handle.style.cursor = 'ns-resize';
|
||||||
|
break;
|
||||||
|
case 'ne':
|
||||||
|
handle.style.top = '-4px';
|
||||||
|
handle.style.right = '-4px';
|
||||||
|
handle.style.cursor = 'nesw-resize';
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
handle.style.top = '50%';
|
||||||
|
handle.style.left = '-4px';
|
||||||
|
handle.style.transform = 'translateY(-50%)';
|
||||||
|
handle.style.cursor = 'ew-resize';
|
||||||
|
break;
|
||||||
|
case 'e':
|
||||||
|
handle.style.top = '50%';
|
||||||
|
handle.style.right = '-4px';
|
||||||
|
handle.style.transform = 'translateY(-50%)';
|
||||||
|
handle.style.cursor = 'ew-resize';
|
||||||
|
break;
|
||||||
|
case 'sw':
|
||||||
|
handle.style.bottom = '-4px';
|
||||||
|
handle.style.left = '-4px';
|
||||||
|
handle.style.cursor = 'nesw-resize';
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
handle.style.bottom = '-4px';
|
||||||
|
handle.style.left = '50%';
|
||||||
|
handle.style.transform = 'translateX(-50%)';
|
||||||
|
handle.style.cursor = 'ns-resize';
|
||||||
|
break;
|
||||||
|
case 'se':
|
||||||
|
handle.style.bottom = '-4px';
|
||||||
|
handle.style.right = '-4px';
|
||||||
|
handle.style.cursor = 'nwse-resize';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标按下事件
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
self._handleResizeStart(e, instance.id, direction);
|
||||||
|
});
|
||||||
|
|
||||||
|
handlesContainer.appendChild(handle);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使组件可拖拽
|
||||||
el.setAttribute('draggable', true);
|
el.setAttribute('draggable', true);
|
||||||
el.addEventListener('dragstart', function(e) {
|
el.addEventListener('dragstart', (e) => {
|
||||||
|
// 如果正在调整大小,不允许拖拽
|
||||||
|
if (self.isResizing) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
self.draggingComponent = instance.id;
|
self.draggingComponent = instance.id;
|
||||||
el.style.opacity = '0.5';
|
el.style.opacity = '0.5';
|
||||||
@@ -337,24 +466,205 @@ var webbuilder = {
|
|||||||
self.dragOffset.x = e.clientX - rect.left;
|
self.dragOffset.x = e.clientX - rect.left;
|
||||||
self.dragOffset.y = e.clientY - rect.top;
|
self.dragOffset.y = e.clientY - rect.top;
|
||||||
});
|
});
|
||||||
el.addEventListener('dragend', function(e) {
|
el.addEventListener('dragend', (e) => {
|
||||||
el.style.opacity = '1';
|
el.style.opacity = '1';
|
||||||
self.draggingComponent = null;
|
self.draggingComponent = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击选中
|
// 点击选中
|
||||||
el.addEventListener('click', function(e) {
|
el.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
self._selectComponent(instance.id);
|
self._selectComponent(instance.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 存储元素引用
|
// 存储元素引用
|
||||||
instance.element = el;
|
instance.element = el;
|
||||||
|
instance.handlesContainer = handlesContainer;
|
||||||
|
|
||||||
// 添加到画布
|
// 添加到画布
|
||||||
this.canvasEl.appendChild(el);
|
this.canvasEl.appendChild(el);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理尺寸调整开始
|
||||||
|
*/
|
||||||
|
_handleResizeStart: function(e, componentId, direction) {
|
||||||
|
var instance = this._findInstance(componentId);
|
||||||
|
if (!instance) return;
|
||||||
|
|
||||||
|
this.isResizing = true;
|
||||||
|
this.resizeDirection = direction;
|
||||||
|
this.resizeStartPos = { x: e.clientX, y: e.clientY };
|
||||||
|
this.resizeStartSize = {
|
||||||
|
colSpan: instance.grid.colSpan,
|
||||||
|
rowSpan: instance.grid.rowSpan,
|
||||||
|
startCell: instance.grid.startCell
|
||||||
|
};
|
||||||
|
|
||||||
|
// 禁用画布的拖拽
|
||||||
|
this.canvasEl.style.pointerEvents = 'none';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理尺寸调整移动
|
||||||
|
*/
|
||||||
|
_handleResizeMove: function(e) {
|
||||||
|
var instance = this._findInstance(this.selectedComponent);
|
||||||
|
if (!instance) return;
|
||||||
|
|
||||||
|
var config = this.phoneConfig;
|
||||||
|
var cellSize = this.cellSize;
|
||||||
|
var gap = config.gap;
|
||||||
|
|
||||||
|
// 计算鼠标移动的距离
|
||||||
|
var deltaX = e.clientX - this.resizeStartPos.x;
|
||||||
|
var deltaY = e.clientY - this.resizeStartPos.y;
|
||||||
|
|
||||||
|
// 计算列和行的变化量
|
||||||
|
var colDelta = Math.round(deltaX / (cellSize + gap));
|
||||||
|
var rowDelta = Math.round(deltaY / (cellSize + gap));
|
||||||
|
|
||||||
|
// 获取起始状态
|
||||||
|
var startCol = this.resizeStartSize.startCell % config.columns;
|
||||||
|
var startRow = Math.floor(this.resizeStartSize.startCell / config.columns);
|
||||||
|
var origColSpan = this.resizeStartSize.colSpan;
|
||||||
|
var origRowSpan = this.resizeStartSize.rowSpan;
|
||||||
|
|
||||||
|
// 计算新的尺寸和位置
|
||||||
|
var newColSpan = origColSpan;
|
||||||
|
var newRowSpan = origRowSpan;
|
||||||
|
var newStartCol = startCol;
|
||||||
|
var newStartRow = startRow;
|
||||||
|
|
||||||
|
switch(this.resizeDirection) {
|
||||||
|
case 'se': // 右下角 - 只改变大小,不改变位置
|
||||||
|
newColSpan = Math.max(1, origColSpan + colDelta);
|
||||||
|
newRowSpan = Math.max(1, origRowSpan + rowDelta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'e': // 右边 - 只改变宽度,不改变位置
|
||||||
|
newColSpan = Math.max(1, origColSpan + colDelta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 's': // 下边 - 只改变高度,不改变位置
|
||||||
|
newRowSpan = Math.max(1, origRowSpan + rowDelta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'w': // 左边 - 改变宽度,同时改变左边位置,右边保持不变
|
||||||
|
newColSpan = Math.max(1, origColSpan - colDelta);
|
||||||
|
newStartCol = startCol + (origColSpan - newColSpan);
|
||||||
|
// 边界检查:确保不超出画布左边界
|
||||||
|
if (newStartCol < 0) {
|
||||||
|
newStartCol = 0;
|
||||||
|
newColSpan = origColSpan + startCol;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sw': // 左下角 - 改变宽度和高度,左边位置改变,下边位置不变
|
||||||
|
newColSpan = Math.max(1, origColSpan - colDelta);
|
||||||
|
newStartCol = startCol + (origColSpan - newColSpan);
|
||||||
|
newRowSpan = Math.max(1, origRowSpan + rowDelta);
|
||||||
|
// 边界检查:确保不超出画布左边界
|
||||||
|
if (newStartCol < 0) {
|
||||||
|
newStartCol = 0;
|
||||||
|
newColSpan = origColSpan + startCol;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'n': // 上边 - 改变高度,同时改变上边位置,下边保持不变
|
||||||
|
newRowSpan = Math.max(1, origRowSpan - rowDelta);
|
||||||
|
newStartRow = startRow + (origRowSpan - newRowSpan);
|
||||||
|
// 边界检查:确保不超出画布上边界
|
||||||
|
if (newStartRow < 0) {
|
||||||
|
newStartRow = 0;
|
||||||
|
newRowSpan = origRowSpan + startRow;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ne': // 右上角 - 改变宽度和高度,右边位置不变,上边位置改变
|
||||||
|
newColSpan = Math.max(1, origColSpan + colDelta);
|
||||||
|
newRowSpan = Math.max(1, origRowSpan - rowDelta);
|
||||||
|
newStartRow = startRow + (origRowSpan - newRowSpan);
|
||||||
|
// 边界检查:确保不超出画布上边界
|
||||||
|
if (newStartRow < 0) {
|
||||||
|
newStartRow = 0;
|
||||||
|
newRowSpan = origRowSpan + startRow;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nw': // 左上角 - 改变宽度和高度,左边和上边位置都改变
|
||||||
|
newColSpan = Math.max(1, origColSpan - colDelta);
|
||||||
|
newStartCol = startCol + (origColSpan - newColSpan);
|
||||||
|
newRowSpan = Math.max(1, origRowSpan - rowDelta);
|
||||||
|
newStartRow = startRow + (origRowSpan - newRowSpan);
|
||||||
|
// 边界检查:确保不超出画布左边界和上边界
|
||||||
|
if (newStartCol < 0) {
|
||||||
|
newStartCol = 0;
|
||||||
|
newColSpan = origColSpan + startCol;
|
||||||
|
}
|
||||||
|
if (newStartRow < 0) {
|
||||||
|
newStartRow = 0;
|
||||||
|
newRowSpan = origRowSpan + startRow;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 边界约束:确保不超出画布右边界和下边界
|
||||||
|
newColSpan = Math.min(newColSpan, config.columns - newStartCol);
|
||||||
|
newRowSpan = Math.min(newRowSpan, this.rows - newStartRow);
|
||||||
|
|
||||||
|
// 最终安全检查:确保尺寸至少为1
|
||||||
|
newColSpan = Math.max(1, newColSpan);
|
||||||
|
newRowSpan = Math.max(1, newRowSpan);
|
||||||
|
|
||||||
|
// 更新组件实例
|
||||||
|
instance.grid.startCell = newStartRow * config.columns + newStartCol;
|
||||||
|
instance.grid.colSpan = newColSpan;
|
||||||
|
instance.grid.rowSpan = newRowSpan;
|
||||||
|
|
||||||
|
// 更新样式
|
||||||
|
instance.element.style.gridColumn = `${newStartCol + 1} / span ${newColSpan}`;
|
||||||
|
instance.element.style.gridRow = `${newStartRow + 1} / span ${newRowSpan}`;
|
||||||
|
|
||||||
|
// 更新属性面板
|
||||||
|
if (this.selectedComponent === instance.id) {
|
||||||
|
this._showPropertyPanel(instance);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理尺寸调整结束
|
||||||
|
*/
|
||||||
|
_handleResizeEnd: function(e) {
|
||||||
|
this.isResizing = false;
|
||||||
|
this.resizeDirection = null;
|
||||||
|
|
||||||
|
// 恢复画布的拖拽
|
||||||
|
this.canvasEl.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
var instance = this._findInstance(this.selectedComponent);
|
||||||
|
if (instance) {
|
||||||
|
this._notifyChanged('update', instance);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示尺寸控制柄
|
||||||
|
*/
|
||||||
|
_showResizeHandles: function(instance) {
|
||||||
|
if (!instance || !instance.handlesContainer) return;
|
||||||
|
instance.handlesContainer.style.display = 'block';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏尺寸控制柄
|
||||||
|
*/
|
||||||
|
_hideResizeHandles: function(instance) {
|
||||||
|
if (!instance || !instance.handlesContainer) return;
|
||||||
|
instance.handlesContainer.style.display = 'none';
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将组件渲染到指定的 div 容器
|
* 将组件渲染到指定的 div 容器
|
||||||
* @param {HTMLElement} container - 目标容器
|
* @param {HTMLElement} container - 目标容器
|
||||||
@@ -368,20 +678,20 @@ var webbuilder = {
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// 创建画布样式
|
// 创建画布样式
|
||||||
container.style.width = this.phoneConfig.width + 'px';
|
container.style.width = `${this.phoneConfig.width}px`;
|
||||||
container.style.height = this.phoneConfig.height + 'px';
|
container.style.height = `${this.phoneConfig.height}px`;
|
||||||
container.style.display = 'grid';
|
container.style.display = 'grid';
|
||||||
container.style.gridTemplateColumns = 'repeat(' + this.phoneConfig.columns + ', ' + this.cellSize + 'px)';
|
container.style.gridTemplateColumns = `repeat(${this.phoneConfig.columns}, ${this.cellSize}px)`;
|
||||||
container.style.gridAutoRows = this.cellSize + 'px';
|
container.style.gridAutoRows = `${this.cellSize}px`;
|
||||||
container.style.gap = this.phoneConfig.gap + 'px';
|
container.style.gap = `${this.phoneConfig.gap}px`;
|
||||||
container.style.background = this.phoneConfig.background;
|
container.style.background = this.phoneConfig.background;
|
||||||
container.style.padding = this.phoneConfig.gap + 'px';
|
container.style.padding = `${this.phoneConfig.gap}px`;
|
||||||
container.style.boxSizing = 'border-box';
|
container.style.boxSizing = 'border-box';
|
||||||
|
|
||||||
var columns = this.phoneConfig.columns;
|
var columns = this.phoneConfig.columns;
|
||||||
|
|
||||||
// 遍历所有组件实例
|
// 遍历所有组件实例
|
||||||
this.componentInstances.forEach(function(instance) {
|
this.componentInstances.forEach((instance) => {
|
||||||
var typeDef = self.componentTypes[instance.type];
|
var typeDef = self.componentTypes[instance.type];
|
||||||
if (!typeDef || !typeDef.render) return;
|
if (!typeDef || !typeDef.render) return;
|
||||||
|
|
||||||
@@ -393,8 +703,8 @@ var webbuilder = {
|
|||||||
|
|
||||||
// 创建组件容器
|
// 创建组件容器
|
||||||
var wrapper = document.createElement('div');
|
var wrapper = document.createElement('div');
|
||||||
wrapper.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
wrapper.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
|
||||||
wrapper.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
|
wrapper.style.gridRow = `${startRow + 1} / span ${rowSpan}`;
|
||||||
wrapper.style.overflow = 'hidden';
|
wrapper.style.overflow = 'hidden';
|
||||||
|
|
||||||
// 调用 render 函数获取组件元素
|
// 调用 render 函数获取组件元素
|
||||||
@@ -441,8 +751,8 @@ var webbuilder = {
|
|||||||
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
||||||
|
|
||||||
// 更新样式
|
// 更新样式
|
||||||
instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
instance.element.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
|
||||||
instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan;
|
instance.element.style.gridRow = `${startRow + 1} / span ${instance.grid.rowSpan}`;
|
||||||
|
|
||||||
// 更新属性面板
|
// 更新属性面板
|
||||||
if (this.selectedComponent === componentId) {
|
if (this.selectedComponent === componentId) {
|
||||||
@@ -466,6 +776,9 @@ var webbuilder = {
|
|||||||
this.selectedComponent = componentId;
|
this.selectedComponent = componentId;
|
||||||
instance.element.classList.add('selected');
|
instance.element.classList.add('selected');
|
||||||
|
|
||||||
|
// 显示尺寸控制柄
|
||||||
|
this._showResizeHandles(instance);
|
||||||
|
|
||||||
// 显示属性面板
|
// 显示属性面板
|
||||||
this._showPropertyPanel(instance);
|
this._showPropertyPanel(instance);
|
||||||
},
|
},
|
||||||
@@ -476,9 +789,17 @@ var webbuilder = {
|
|||||||
_deselectAll: function() {
|
_deselectAll: function() {
|
||||||
this.selectedComponent = null;
|
this.selectedComponent = null;
|
||||||
var selected = this.canvasEl.querySelectorAll('.selected');
|
var selected = this.canvasEl.querySelectorAll('.selected');
|
||||||
selected.forEach(function(el) {
|
selected.forEach((el) => {
|
||||||
el.classList.remove('selected');
|
el.classList.remove('selected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 隐藏所有尺寸控制柄
|
||||||
|
this.componentInstances.forEach((instance) => {
|
||||||
|
if (instance.handlesContainer) {
|
||||||
|
instance.handlesContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.propertyPanelEl.innerHTML = '';
|
this.propertyPanelEl.innerHTML = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -494,31 +815,31 @@ var webbuilder = {
|
|||||||
var startCol = instance.grid.startCell % columns;
|
var startCol = instance.grid.startCell % columns;
|
||||||
var startRow = Math.floor(instance.grid.startCell / columns);
|
var startRow = Math.floor(instance.grid.startCell / columns);
|
||||||
|
|
||||||
var html = '<div class="property-title">' + (typeDef.label || instance.type) + '</div>';
|
var html = `<div class="property-title">${typeDef.label || instance.type}</div>`;
|
||||||
|
|
||||||
// 基础属性
|
// 基础属性
|
||||||
html += '<div class="property-section">';
|
html += '<div class="property-section">';
|
||||||
html += '<div class="property-section-title">位置大小</div>';
|
html += '<div class="property-section-title">位置大小</div>';
|
||||||
html += '<div class="property-row"><label>起始列:</label><input type="number" data-prop="startCol" value="' + (startCol + 1) + '" min="1" max="' + columns + '"></div>';
|
html += `<div class="property-row"><label>起始列:</label><input type="number" data-prop="startCol" value="${startCol + 1}" min="1" max="${columns}"></div>`;
|
||||||
html += '<div class="property-row"><label>起始行:</label><input type="number" data-prop="startRow" value="' + (startRow + 1) + '" min="1"></div>';
|
html += `<div class="property-row"><label>起始行:</label><input type="number" data-prop="startRow" value="${startRow + 1}" min="1"></div>`;
|
||||||
html += '<div class="property-row"><label>跨列数:</label><input type="number" data-prop="colSpan" value="' + instance.grid.colSpan + '" min="1" max="' + columns + '"></div>';
|
html += `<div class="property-row"><label>跨列数:</label><input type="number" data-prop="colSpan" value="${instance.grid.colSpan}" min="1" max="${columns}"></div>`;
|
||||||
html += '<div class="property-row"><label>跨行数:</label><input type="number" data-prop="rowSpan" value="' + instance.grid.rowSpan + '" min="1"></div>';
|
html += `<div class="property-row"><label>跨行数:</label><input type="number" data-prop="rowSpan" value="${instance.grid.rowSpan}" min="1"></div>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// 自定义属性
|
// 自定义属性
|
||||||
if (typeDef.traits && typeDef.traits.length > 0) {
|
if (typeDef.traits && typeDef.traits.length > 0) {
|
||||||
html += '<div class="property-section">';
|
html += '<div class="property-section">';
|
||||||
html += '<div class="property-section-title">组件属性</div>';
|
html += '<div class="property-section-title">组件属性</div>';
|
||||||
typeDef.traits.forEach(function(trait) {
|
typeDef.traits.forEach((trait) => {
|
||||||
var value = instance.props[trait.name] !== undefined ? instance.props[trait.name] : (trait.default || '');
|
var value = instance.props[trait.name] !== undefined ? instance.props[trait.name] : (trait.default || '');
|
||||||
html += '<div class="property-row">';
|
html += '<div class="property-row">';
|
||||||
html += '<label>' + trait.label + ':</label>';
|
html += `<label>${trait.label}:</label>`;
|
||||||
if (trait.type === 'number') {
|
if (trait.type === 'number') {
|
||||||
html += '<input type="number" data-trait="' + trait.name + '" value="' + value + '">';
|
html += `<input type="number" data-trait="${trait.name}" value="${value}">`;
|
||||||
} else if (trait.type === 'checkbox') {
|
} else if (trait.type === 'checkbox') {
|
||||||
html += '<input type="checkbox" data-trait="' + trait.name + '"' + (value ? ' checked' : '') + '>';
|
html += `<input type="checkbox" data-trait="${trait.name}"${value ? ' checked' : ''}>`;
|
||||||
} else {
|
} else {
|
||||||
html += '<input type="text" data-trait="' + trait.name + '" value="' + value + '">';
|
html += `<input type="text" data-trait="${trait.name}" value="${value}">`;
|
||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
});
|
});
|
||||||
@@ -533,8 +854,8 @@ var webbuilder = {
|
|||||||
this.propertyPanelEl.innerHTML = html;
|
this.propertyPanelEl.innerHTML = html;
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
this.propertyPanelEl.querySelectorAll('input').forEach(function(input) {
|
this.propertyPanelEl.querySelectorAll('input').forEach((input) => {
|
||||||
input.addEventListener('change', function() {
|
input.addEventListener('change', () => {
|
||||||
var prop = input.getAttribute('data-prop');
|
var prop = input.getAttribute('data-prop');
|
||||||
var trait = input.getAttribute('data-trait');
|
var trait = input.getAttribute('data-trait');
|
||||||
|
|
||||||
@@ -558,12 +879,12 @@ var webbuilder = {
|
|||||||
var startRow = Math.floor(instance.grid.startCell / columns);
|
var startRow = Math.floor(instance.grid.startCell / columns);
|
||||||
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
||||||
|
|
||||||
instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
instance.element.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
|
||||||
instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan;
|
instance.element.style.gridRow = `${startRow + 1} / span ${instance.grid.rowSpan}`;
|
||||||
} else if (trait) {
|
} else if (trait) {
|
||||||
// 更新组件属性
|
// 更新组件属性
|
||||||
var typeDef = self.componentTypes[instance.type];
|
var typeDef = self.componentTypes[instance.type];
|
||||||
var traitDef = typeDef.traits.find(function(t) { return t.name === trait; });
|
var traitDef = typeDef.traits.find((t) => t.name === trait);
|
||||||
|
|
||||||
if (input.type === 'checkbox') {
|
if (input.type === 'checkbox') {
|
||||||
instance.props[trait] = input.checked;
|
instance.props[trait] = input.checked;
|
||||||
@@ -582,6 +903,22 @@ var webbuilder = {
|
|||||||
} else if (content instanceof HTMLElement) {
|
} else if (content instanceof HTMLElement) {
|
||||||
instance.element.appendChild(content);
|
instance.element.appendChild(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重新添加尺寸控制柄容器
|
||||||
|
var handlesContainer = document.createElement('div');
|
||||||
|
handlesContainer.className = 'resize-handles-container';
|
||||||
|
handlesContainer.style.display = 'block';
|
||||||
|
handlesContainer.style.position = 'absolute';
|
||||||
|
handlesContainer.style.top = '0';
|
||||||
|
handlesContainer.style.left = '0';
|
||||||
|
handlesContainer.style.width = '100%';
|
||||||
|
handlesContainer.style.height = '100%';
|
||||||
|
handlesContainer.style.pointerEvents = 'none';
|
||||||
|
instance.element.appendChild(handlesContainer);
|
||||||
|
instance.handlesContainer = handlesContainer;
|
||||||
|
|
||||||
|
// 重新创建控制柄
|
||||||
|
self._recreateResizeHandles(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,12 +930,90 @@ var webbuilder = {
|
|||||||
// 删除按钮
|
// 删除按钮
|
||||||
var deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]');
|
var deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]');
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
deleteBtn.addEventListener('click', function() {
|
deleteBtn.addEventListener('click', () => {
|
||||||
self._deleteComponent(instance.id);
|
self._deleteComponent(instance.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新创建尺寸控制柄
|
||||||
|
*/
|
||||||
|
_recreateResizeHandles: function(instance) {
|
||||||
|
if (!instance || !instance.handlesContainer) return;
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
|
||||||
|
|
||||||
|
// 清空控制柄容器
|
||||||
|
instance.handlesContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// 重新创建8个控制柄
|
||||||
|
directions.forEach((direction) => {
|
||||||
|
var handle = document.createElement('div');
|
||||||
|
handle.className = `resize-handle resize-handle-${direction}`;
|
||||||
|
handle.setAttribute('data-direction', direction);
|
||||||
|
handle.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
|
// 控制柄位置
|
||||||
|
switch(direction) {
|
||||||
|
case 'nw':
|
||||||
|
handle.style.top = '-4px';
|
||||||
|
handle.style.left = '-4px';
|
||||||
|
handle.style.cursor = 'nwse-resize';
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
handle.style.top = '-4px';
|
||||||
|
handle.style.left = '50%';
|
||||||
|
handle.style.transform = 'translateX(-50%)';
|
||||||
|
handle.style.cursor = 'ns-resize';
|
||||||
|
break;
|
||||||
|
case 'ne':
|
||||||
|
handle.style.top = '-4px';
|
||||||
|
handle.style.right = '-4px';
|
||||||
|
handle.style.cursor = 'nesw-resize';
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
handle.style.top = '50%';
|
||||||
|
handle.style.left = '-4px';
|
||||||
|
handle.style.transform = 'translateY(-50%)';
|
||||||
|
handle.style.cursor = 'ew-resize';
|
||||||
|
break;
|
||||||
|
case 'e':
|
||||||
|
handle.style.top = '50%';
|
||||||
|
handle.style.right = '-4px';
|
||||||
|
handle.style.transform = 'translateY(-50%)';
|
||||||
|
handle.style.cursor = 'ew-resize';
|
||||||
|
break;
|
||||||
|
case 'sw':
|
||||||
|
handle.style.bottom = '-4px';
|
||||||
|
handle.style.left = '-4px';
|
||||||
|
handle.style.cursor = 'nesw-resize';
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
handle.style.bottom = '-4px';
|
||||||
|
handle.style.left = '50%';
|
||||||
|
handle.style.transform = 'translateX(-50%)';
|
||||||
|
handle.style.cursor = 'ns-resize';
|
||||||
|
break;
|
||||||
|
case 'se':
|
||||||
|
handle.style.bottom = '-4px';
|
||||||
|
handle.style.right = '-4px';
|
||||||
|
handle.style.cursor = 'nwse-resize';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标按下事件
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
self._handleResizeStart(e, instance.id, direction);
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.handlesContainer.appendChild(handle);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除组件
|
* 删除组件
|
||||||
*/
|
*/
|
||||||
@@ -673,14 +1088,14 @@ var webbuilder = {
|
|||||||
var icon = definition.icon || '📦';
|
var icon = definition.icon || '📦';
|
||||||
var label = definition.label || name;
|
var label = definition.label || name;
|
||||||
|
|
||||||
item.innerHTML = '<span class="icon">' + icon + '</span><span class="label">' + label + '</span>';
|
item.innerHTML = `<span class="icon">${icon}</span><span class="label">${label}</span>`;
|
||||||
|
|
||||||
// 拖拽事件
|
// 拖拽事件
|
||||||
item.addEventListener('dragstart', function(e) {
|
item.addEventListener('dragstart', () => {
|
||||||
self.draggingType = name;
|
self.draggingType = name;
|
||||||
item.style.opacity = '0.5';
|
item.style.opacity = '0.5';
|
||||||
});
|
});
|
||||||
item.addEventListener('dragend', function(e) {
|
item.addEventListener('dragend', () => {
|
||||||
item.style.opacity = '1';
|
item.style.opacity = '1';
|
||||||
self.draggingType = null;
|
self.draggingType = null;
|
||||||
});
|
});
|
||||||
@@ -693,7 +1108,7 @@ var webbuilder = {
|
|||||||
* 返回格式: { version, layouts: { phone: {...}, computer: null } }
|
* 返回格式: { version, layouts: { phone: {...}, computer: null } }
|
||||||
*/
|
*/
|
||||||
toJSONB: function() {
|
toJSONB: function() {
|
||||||
var components = this.componentInstances.map(function(instance) {
|
var components = this.componentInstances.map((instance) => {
|
||||||
return {
|
return {
|
||||||
id: instance.id,
|
id: instance.id,
|
||||||
type: instance.type,
|
type: instance.type,
|
||||||
@@ -745,7 +1160,7 @@ var webbuilder = {
|
|||||||
// 渲染组件
|
// 渲染组件
|
||||||
if (phoneLayout.components) {
|
if (phoneLayout.components) {
|
||||||
var self = this;
|
var self = this;
|
||||||
phoneLayout.components.forEach(function(comp) {
|
phoneLayout.components.forEach((comp) => {
|
||||||
var typeDef = self.componentTypes[comp.type];
|
var typeDef = self.componentTypes[comp.type];
|
||||||
if (!typeDef) {
|
if (!typeDef) {
|
||||||
console.warn('Component type not found:', comp.type);
|
console.warn('Component type not found:', comp.type);
|
||||||
@@ -769,7 +1184,7 @@ var webbuilder = {
|
|||||||
|
|
||||||
// 更新ID计数器
|
// 更新ID计数器
|
||||||
var maxId = 0;
|
var maxId = 0;
|
||||||
this.componentInstances.forEach(function(inst) {
|
this.componentInstances.forEach((inst) => {
|
||||||
var match = inst.id.match(/-(\d+)$/);
|
var match = inst.id.match(/-(\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
maxId = Math.max(maxId, parseInt(match[1], 10));
|
maxId = Math.max(maxId, parseInt(match[1], 10));
|
||||||
|
|||||||
Reference in New Issue
Block a user