diff --git a/css/editor.css b/css/editor.css index 0370453..8da16db 100644 --- a/css/editor.css +++ b/css/editor.css @@ -189,6 +189,8 @@ body { border-radius: 4px; transition: border-color 0.3s; cursor: move; + position: relative; + overflow: visible; } .webbuilder-component:hover { @@ -200,6 +202,67 @@ body { 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 { width: 280px; diff --git a/js/lib/webbuilder/webbuilder.js b/js/lib/webbuilder/webbuilder.js index 74160f3..7e259da 100644 --- a/js/lib/webbuilder/webbuilder.js +++ b/js/lib/webbuilder/webbuilder.js @@ -43,6 +43,13 @@ var webbuilder = { // 变化回调函数 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._initResizeEvents(); + return this; }, @@ -134,34 +144,34 @@ var webbuilder = { this.canvasEl.style.width = config.width + 'px'; this.canvasEl.style.height = config.height + 'px'; this.canvasEl.style.display = 'grid'; - this.canvasEl.style.gridTemplateColumns = 'repeat(' + config.columns + ', ' + this.cellSize + 'px)'; - this.canvasEl.style.gridAutoRows = this.cellSize + 'px'; - this.canvasEl.style.gap = config.gap + 'px'; + this.canvasEl.style.gridTemplateColumns = `repeat(${config.columns}, ${this.cellSize}px)`; + this.canvasEl.style.gridAutoRows = `${this.cellSize}px`; + this.canvasEl.style.gap = `${config.gap}px`; this.canvasEl.style.background = config.background; 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.canvasWrapperEl.appendChild(this.canvasEl); // 画布点击取消选中 var self = this; - this.canvasEl.addEventListener('click', function(e) { + this.canvasEl.addEventListener('click', (e) => { if (e.target === self.canvasEl) { self._deselectAll(); } }); // 拖放事件 - this.canvasEl.addEventListener('dragover', function(e) { + this.canvasEl.addEventListener('dragover', (e) => { e.preventDefault(); self.canvasEl.classList.add('dropping'); }); - this.canvasEl.addEventListener('dragleave', function(e) { + this.canvasEl.addEventListener('dragleave', (e) => { self.canvasEl.classList.remove('dropping'); }); - this.canvasEl.addEventListener('drop', function(e) { + this.canvasEl.addEventListener('drop', (e) => { e.preventDefault(); self.canvasEl.classList.remove('dropping'); @@ -194,7 +204,18 @@ var webbuilder = { */ _initKeyboardEvents: function() { 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键删除选中的组件 if (e.key === 'Delete' || e.key === 'Backspace') { 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) @@ -308,10 +352,10 @@ var webbuilder = { el.className = 'webbuilder-component'; el.setAttribute('data-id', instance.id); el.setAttribute('data-type', instance.type); - el.style.gridColumn = (startCol + 1) + ' / span ' + colSpan; - el.style.gridRow = (startRow + 1) + ' / span ' + rowSpan; + el.style.gridColumn = `${startCol + 1} / span ${colSpan}`; + el.style.gridRow = `${startRow + 1} / span ${rowSpan}`; el.style.position = 'relative'; - el.style.overflow = 'hidden'; + el.style.overflow = 'visible'; // 渲染组件内容 - render 函数返回 HTML 元素对象 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; + + 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.addEventListener('dragstart', function(e) { + el.addEventListener('dragstart', (e) => { + // 如果正在调整大小,不允许拖拽 + if (self.isResizing) { + e.preventDefault(); + return; + } + e.stopPropagation(); self.draggingComponent = instance.id; el.style.opacity = '0.5'; @@ -337,24 +466,205 @@ var webbuilder = { self.dragOffset.x = e.clientX - rect.left; self.dragOffset.y = e.clientY - rect.top; }); - el.addEventListener('dragend', function(e) { + el.addEventListener('dragend', (e) => { el.style.opacity = '1'; self.draggingComponent = null; }); // 点击选中 - el.addEventListener('click', function(e) { + el.addEventListener('click', (e) => { e.stopPropagation(); self._selectComponent(instance.id); }); // 存储元素引用 instance.element = el; + instance.handlesContainer = handlesContainer; // 添加到画布 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 容器 * @param {HTMLElement} container - 目标容器 @@ -368,20 +678,20 @@ var webbuilder = { container.innerHTML = ''; // 创建画布样式 - container.style.width = this.phoneConfig.width + 'px'; - container.style.height = this.phoneConfig.height + 'px'; + container.style.width = `${this.phoneConfig.width}px`; + container.style.height = `${this.phoneConfig.height}px`; container.style.display = 'grid'; - container.style.gridTemplateColumns = 'repeat(' + this.phoneConfig.columns + ', ' + this.cellSize + 'px)'; - container.style.gridAutoRows = this.cellSize + 'px'; - container.style.gap = this.phoneConfig.gap + 'px'; + container.style.gridTemplateColumns = `repeat(${this.phoneConfig.columns}, ${this.cellSize}px)`; + container.style.gridAutoRows = `${this.cellSize}px`; + container.style.gap = `${this.phoneConfig.gap}px`; 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'; var columns = this.phoneConfig.columns; // 遍历所有组件实例 - this.componentInstances.forEach(function(instance) { + this.componentInstances.forEach((instance) => { var typeDef = self.componentTypes[instance.type]; if (!typeDef || !typeDef.render) return; @@ -393,8 +703,8 @@ var webbuilder = { // 创建组件容器 var wrapper = document.createElement('div'); - wrapper.style.gridColumn = (startCol + 1) + ' / span ' + colSpan; - wrapper.style.gridRow = (startRow + 1) + ' / span ' + rowSpan; + wrapper.style.gridColumn = `${startCol + 1} / span ${colSpan}`; + wrapper.style.gridRow = `${startRow + 1} / span ${rowSpan}`; wrapper.style.overflow = 'hidden'; // 调用 render 函数获取组件元素 @@ -441,8 +751,8 @@ var webbuilder = { var colSpan = Math.min(instance.grid.colSpan, columns - startCol); // 更新样式 - instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan; - instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan; + instance.element.style.gridColumn = `${startCol + 1} / span ${colSpan}`; + instance.element.style.gridRow = `${startRow + 1} / span ${instance.grid.rowSpan}`; // 更新属性面板 if (this.selectedComponent === componentId) { @@ -466,6 +776,9 @@ var webbuilder = { this.selectedComponent = componentId; instance.element.classList.add('selected'); + // 显示尺寸控制柄 + this._showResizeHandles(instance); + // 显示属性面板 this._showPropertyPanel(instance); }, @@ -476,9 +789,17 @@ var webbuilder = { _deselectAll: function() { this.selectedComponent = null; var selected = this.canvasEl.querySelectorAll('.selected'); - selected.forEach(function(el) { + selected.forEach((el) => { el.classList.remove('selected'); }); + + // 隐藏所有尺寸控制柄 + this.componentInstances.forEach((instance) => { + if (instance.handlesContainer) { + instance.handlesContainer.style.display = 'none'; + } + }); + this.propertyPanelEl.innerHTML = ''; }, @@ -494,31 +815,31 @@ var webbuilder = { var startCol = instance.grid.startCell % columns; var startRow = Math.floor(instance.grid.startCell / columns); - var html = '