/**
* WebBuilder - IoT 可视化编辑器
* 支持手机端和电脑端布局
* 单元格比例 1:1(正方形)
*/
var webbuilder = {
container: null,
phoneConfig: {
width: 375,
height: 812,
columns: 24,
gap: 2,
background: '#f5f5f5'
},
cellSize: 0,
rows: 0,
componentTypes: {},
componentInstances: [],
selectedComponent: null,
draggingComponent: null,
draggingType: null,
dragOffset: { x: 0, y: 0 },
idCounter: 0,
onChangedCallback: null,
resizeHandles: null,
isResizing: false,
resizeDirection: null,
resizeStartPos: { x: 0, y: 0 },
resizeStartSize: { colSpan: 0, rowSpan: 0 },
isMobile: false,
mobilePanelMode: 'toolbox',
touchDragType: null,
touchDragComponent: null,
touchDragOffset: { x: 0, y: 0 },
mobileStyleEl: null,
_applyHandleBaseStyle: function(handle) {
handle.style.position = 'absolute';
handle.style.width = '8px';
handle.style.height = '8px';
handle.style.background = '#1890ff';
handle.style.border = '1px solid #fff';
handle.style.borderRadius = '2px';
handle.style.pointerEvents = 'auto';
handle.style.zIndex = '11';
handle.style.boxShadow = '0 0 2px rgba(0, 0, 0, 0.3)';
},
_calculateGrid: function() {
var config = this.phoneConfig;
this.cellSize = (config.width - (config.columns + 1) * config.gap) / config.columns;
this.rows = Math.floor((config.height - config.gap) / (this.cellSize + config.gap));
this.phoneConfig.height = this.rows * this.cellSize + (this.rows + 1) * config.gap;
},
_detectMobile: function() {
return window.innerWidth <= 768 ||
'ontouchstart' in window ||
navigator.maxTouchPoints > 0;
},
_injectMobileStyles: function() {
if (this.mobileStyleEl) return;
var style = document.createElement('style');
style.textContent = [
'.wb-mobile {',
' display: flex !important;',
' flex-direction: column !important;',
' height: 100vh !important;',
'}',
'.wb-mobile .webbuilder-canvas-wrapper {',
' flex: 1 !important;',
' overflow: auto !important;',
' min-height: 0 !important;',
'}',
'.wb-mobile .webbuilder-toolbox {',
' width: 100% !important;',
' height: 80px !important;',
' flex-shrink: 0 !important;',
' border-right: none !important;',
' border-top: 1px solid #e8e8e8 !important;',
'}',
'.wb-mobile .webbuilder-toolbox-title {',
' display: none !important;',
'}',
'.wb-mobile .webbuilder-toolbox-list {',
' display: flex !important;',
' flex-direction: row !important;',
' overflow-x: auto !important;',
' overflow-y: hidden !important;',
' padding: 8px !important;',
' gap: 8px !important;',
' height: 100% !important;',
' -webkit-overflow-scrolling: touch !important;',
'}',
'.wb-mobile .webbuilder-toolbox-item {',
' flex-direction: column !important;',
' min-width: 64px !important;',
' padding: 8px !important;',
' margin-bottom: 0 !important;',
' justify-content: center !important;',
' align-items: center !important;',
'}',
'.wb-mobile .webbuilder-toolbox-item .icon {',
' font-size: 24px !important;',
'}',
'.wb-mobile .webbuilder-toolbox-item .label {',
' font-size: 11px !important;',
' text-align: center !important;',
'}',
'.wb-mobile .webbuilder-property-panel {',
' width: 100% !important;',
' height: 200px !important;',
' flex-shrink: 0 !important;',
' border-left: none !important;',
' border-top: 1px solid #e8e8e8 !important;',
' overflow-y: auto !important;',
' background: #fff !important;',
'}',
'.wb-mobile-hidden {',
' display: none !important;',
'}',
'.wb-mobile-close-panel {',
' width: 100% !important;',
' padding: 10px !important;',
' margin: 8px 0 !important;',
' background: #1890ff !important;',
' border: none !important;',
' border-radius: 4px !important;',
' color: #fff !important;',
' font-size: 14px !important;',
' cursor: pointer !important;',
'}'
].join('\n');
document.head.appendChild(style);
this.mobileStyleEl = style;
},
init: function(containerSelector) {
if (typeof containerSelector === 'string') {
this.container = document.querySelector(containerSelector);
} else {
this.container = containerSelector;
}
if (!this.container) {
throw new Error('Container element not found');
}
this.isMobile = this._detectMobile();
if (this.isMobile) {
this._injectMobileStyles();
}
this._calculateGrid();
this._createEditorStructure();
this._initCanvas();
this._initToolbox();
this._initKeyboardEvents();
this._initResizeEvents();
this._initTouchEvents();
if (this.isMobile) {
this._updateMobilePanelVisibility();
}
return this;
},
_createEditorStructure: function() {
this.container.innerHTML = '';
this.container.classList.add('webbuilder-editor');
if (this.isMobile) {
this.container.classList.add('wb-mobile');
}
this.canvasWrapperEl = document.createElement('div');
this.canvasWrapperEl.className = 'webbuilder-canvas-wrapper';
this.container.appendChild(this.canvasWrapperEl);
this.propertyPanelEl = document.createElement('div');
this.propertyPanelEl.className = 'webbuilder-property-panel';
this.container.appendChild(this.propertyPanelEl);
this.toolboxEl = document.createElement('div');
this.toolboxEl.className = 'webbuilder-toolbox';
this.container.appendChild(this.toolboxEl);
},
_initCanvas: function() {
var config = this.phoneConfig;
var self = this;
this.canvasEl = document.createElement('div');
this.canvasEl.className = 'webbuilder-canvas';
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.background = config.background;
this.canvasEl.style.position = 'relative';
this.canvasEl.style.padding = config.gap + 'px';
this.canvasEl.style.boxSizing = 'border-box';
this.canvasWrapperEl.appendChild(this.canvasEl);
this.canvasEl.addEventListener('click', function(e) {
if (e.target === self.canvasEl) {
self._deselectAll();
}
});
this.canvasEl.addEventListener('dragover', function(e) {
e.preventDefault();
self.canvasEl.classList.add('dropping');
});
this.canvasEl.addEventListener('dragleave', function() {
self.canvasEl.classList.remove('dropping');
});
this.canvasEl.addEventListener('drop', function(e) {
var gridPos;
var gridPos2;
e.preventDefault();
self.canvasEl.classList.remove('dropping');
if (self.draggingType) {
gridPos = self._calculateGridPosition(e);
self._addComponentToCanvas(self.draggingType, gridPos);
self.draggingType = null;
} else if (self.draggingComponent) {
gridPos2 = self._calculateGridPosition(e);
self._moveComponent(self.draggingComponent, gridPos2);
self.draggingComponent = null;
}
});
},
_initToolbox: function() {
if (this.isMobile) {
this._initMobileToolbox();
} else {
this._initDesktopToolbox();
}
},
_initDesktopToolbox: function() {
this.toolboxEl.innerHTML = '
组件库
';
this.toolboxListEl = document.createElement('div');
this.toolboxListEl.className = 'webbuilder-toolbox-list';
this.toolboxEl.appendChild(this.toolboxListEl);
},
_initMobileToolbox: function() {
this.toolboxListEl = document.createElement('div');
this.toolboxListEl.className = 'webbuilder-toolbox-list wb-mobile-toolbox-list';
this.toolboxEl.appendChild(this.toolboxListEl);
},
_initKeyboardEvents: function() {
var self = this;
document.addEventListener('keydown', function(e) {
var target = e.target;
var isInputElement = target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable;
if (isInputElement) {
return;
}
if (e.key === 'Delete' || e.key === 'Backspace') {
if (self.selectedComponent) {
e.preventDefault();
self._deleteComponent(self.selectedComponent);
}
}
});
},
_initResizeEvents: function() {
var self = this;
document.addEventListener('mousemove', function(e) {
if (self.isResizing && self.selectedComponent) {
e.preventDefault();
self._handleResizeMove(e);
}
});
document.addEventListener('mouseup', function(e) {
if (self.isResizing) {
e.preventDefault();
self._handleResizeEnd(e);
}
});
},
_initTouchEvents: function() {
var self = this;
if (!this.isMobile) return;
this.toolboxListEl.addEventListener('touchstart', function(e) {
var item = e.target.closest('.webbuilder-toolbox-item');
if (!item) return;
self.touchDragType = item.getAttribute('data-type');
}, { passive: true });
this.canvasEl.addEventListener('touchmove', function(e) {
if (self.touchDragType || self.touchDragComponent) {
e.preventDefault();
}
}, { passive: false });
this.canvasEl.addEventListener('touchend', function(e) {
var touch;
var gridPos;
var touch2;
var gridPos2;
if (self.touchDragType) {
touch = e.changedTouches[0];
gridPos = self._calculateGridPositionFromTouch(touch);
if (gridPos) {
self._addComponentToCanvas(self.touchDragType, gridPos);
}
self.touchDragType = null;
} else if (self.touchDragComponent) {
touch2 = e.changedTouches[0];
gridPos2 = self._calculateGridPositionFromTouch(touch2);
if (gridPos2) {
self._moveComponent(self.touchDragComponent, gridPos2);
}
self.touchDragComponent = null;
}
});
},
_calculateGridPositionFromTouch: function(touch) {
var rect = this.canvasEl.getBoundingClientRect();
var config = this.phoneConfig;
var x = touch.clientX - rect.left - config.gap;
var y = touch.clientY - rect.top - config.gap;
var col;
var row;
if (x < 0 || y < 0 || x > config.width || y > config.height) {
return null;
}
col = Math.floor(x / (this.cellSize + config.gap)) + 1;
row = Math.floor(y / (this.cellSize + config.gap)) + 1;
col = Math.max(1, Math.min(config.columns, col));
row = Math.max(1, Math.min(this.rows, row));
return { column: col, row: row };
},
onChanged: function(callback) {
this.onChangedCallback = callback;
},
_notifyChanged: function(action, data) {
if (typeof this.onChangedCallback === 'function') {
this.onChangedCallback(action, data);
}
},
_calculateGridPosition: function(e) {
var rect = this.canvasEl.getBoundingClientRect();
var config = this.phoneConfig;
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;
var row = Math.floor(y / (this.cellSize + config.gap)) + 1;
col = Math.max(1, Math.min(config.columns, col));
row = Math.max(1, Math.min(this.rows, row));
return { column: col, row: row };
},
_addComponentToCanvas: function(componentType, gridPos) {
var typeDef = this.componentTypes[componentType];
var id;
var startCell;
var instance;
if (!typeDef) {
console.error('Component type not found:', componentType);
return;
}
id = componentType + '-' + (++this.idCounter);
startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
instance = {
id: id,
type: componentType,
grid: {
startCell: startCell,
colSpan: typeDef.defaultColumnSpan || 6,
rowSpan: typeDef.defaultRowSpan || 4
},
props: JSON.parse(JSON.stringify(typeDef.defaultProps || {}))
};
this._renderComponent(instance);
this.componentInstances.push(instance);
this._selectComponent(instance.id);
this._notifyChanged('add', instance);
},
_renderComponent: function(instance) {
var typeDef = this.componentTypes[instance.type];
var columns;
var startCol;
var startRow;
var colSpan;
var rowSpan;
var el;
var content;
var handlesContainer;
var directions;
var self = this;
if (!typeDef) return;
columns = this.phoneConfig.columns;
startCol = instance.grid.startCell % columns;
startRow = Math.floor(instance.grid.startCell / columns);
colSpan = Math.min(instance.grid.colSpan, columns - startCol);
rowSpan = instance.grid.rowSpan;
el = document.createElement('div');
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.position = 'relative';
el.style.overflow = 'visible';
if (typeDef.render) {
content = typeDef.render(instance.props);
if (typeof content === 'string') {
el.innerHTML = content;
} else if (content instanceof HTMLElement) {
el.appendChild(content);
}
}
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';
handlesContainer.style.zIndex = '10';
el.appendChild(handlesContainer);
directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
directions.forEach(function(direction) {
var handle = document.createElement('div');
handle.className = 'resize-handle resize-handle-' + direction;
handle.setAttribute('data-direction', direction);
self._applyHandleBaseStyle(handle);
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', function(e) {
e.stopPropagation();
e.preventDefault();
self._handleResizeStart(e, instance.id, direction);
});
handlesContainer.appendChild(handle);
});
el.setAttribute('draggable', true);
el.addEventListener('dragstart', function(e) {
var rect;
if (self.isResizing) {
e.preventDefault();
return;
}
e.stopPropagation();
self.draggingComponent = instance.id;
el.style.opacity = '0.5';
rect = el.getBoundingClientRect();
self.dragOffset.x = e.clientX - rect.left;
self.dragOffset.y = e.clientY - rect.top;
});
el.addEventListener('dragend', function() {
el.style.opacity = '1';
self.draggingComponent = null;
});
el.addEventListener('click', function(e) {
e.stopPropagation();
self._selectComponent(instance.id);
});
if (this.isMobile) {
el.addEventListener('touchstart', function(e) {
var touch;
var rect;
e.stopPropagation();
self.touchDragComponent = instance.id;
touch = e.touches[0];
rect = el.getBoundingClientRect();
self.touchDragOffset.x = touch.clientX - rect.left;
self.touchDragOffset.y = touch.clientY - rect.top;
}, { passive: true });
}
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);
var config;
var cellSize;
var gap;
var deltaX;
var deltaY;
var colDelta;
var rowDelta;
var startCol;
var startRow;
var origColSpan;
var origRowSpan;
var newColSpan;
var newRowSpan;
var newStartCol;
var newStartRow;
if (!instance) return;
config = this.phoneConfig;
cellSize = this.cellSize;
gap = config.gap;
deltaX = e.clientX - this.resizeStartPos.x;
deltaY = e.clientY - this.resizeStartPos.y;
colDelta = Math.round(deltaX / (cellSize + gap));
rowDelta = Math.round(deltaY / (cellSize + gap));
startCol = this.resizeStartSize.startCell % config.columns;
startRow = Math.floor(this.resizeStartSize.startCell / config.columns);
origColSpan = this.resizeStartSize.colSpan;
origRowSpan = this.resizeStartSize.rowSpan;
newColSpan = origColSpan;
newRowSpan = origRowSpan;
newStartCol = startCol;
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);
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() {
var instance = this._findInstance(this.selectedComponent);
this.isResizing = false;
this.resizeDirection = null;
this.canvasEl.style.pointerEvents = 'auto';
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';
},
renderToDiv: function(container) {
var self = this;
var result = [];
var columns;
container.innerHTML = '';
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.background = this.phoneConfig.background;
container.style.padding = this.phoneConfig.gap + 'px';
container.style.boxSizing = 'border-box';
columns = this.phoneConfig.columns;
this.componentInstances.forEach(function(instance) {
var typeDef = self.componentTypes[instance.type];
var startCol;
var startRow;
var colSpan;
var rowSpan;
var wrapper;
var componentElement;
if (!typeDef || !typeDef.render) return;
startCol = instance.grid.startCell % columns;
startRow = Math.floor(instance.grid.startCell / columns);
colSpan = Math.min(instance.grid.colSpan, columns - startCol);
rowSpan = instance.grid.rowSpan;
wrapper = document.createElement('div');
wrapper.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
wrapper.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
wrapper.style.overflow = 'hidden';
componentElement = typeDef.render(instance.props);
if (typeof componentElement === 'string') {
wrapper.innerHTML = componentElement;
componentElement = wrapper.firstChild;
} else if (componentElement instanceof HTMLElement) {
wrapper.appendChild(componentElement);
}
container.appendChild(wrapper);
result.push({
element: componentElement,
wrapper: wrapper,
props: JSON.parse(JSON.stringify(instance.props)),
id: instance.id,
type: instance.type
});
});
return result;
},
_moveComponent: function(componentId, gridPos) {
var instance = this._findInstance(componentId);
var columns;
var startCol;
var startRow;
var colSpan;
if (!instance) return;
instance.grid.startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
columns = this.phoneConfig.columns;
startCol = instance.grid.startCell % columns;
startRow = Math.floor(instance.grid.startCell / columns);
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;
if (this.selectedComponent === componentId) {
this._showPropertyPanel(instance);
}
this._notifyChanged('move', instance);
},
_selectComponent: function(componentId) {
var instance;
this._deselectAll();
instance = this._findInstance(componentId);
if (!instance) return;
this.selectedComponent = componentId;
instance.element.classList.add('selected');
this._showResizeHandles(instance);
this._showPropertyPanel(instance);
if (this.isMobile) {
this.mobilePanelMode = 'property';
this._updateMobilePanelVisibility();
}
},
_deselectAll: function() {
var selected = this.canvasEl.querySelectorAll('.selected');
var self = this;
this.selectedComponent = null;
selected.forEach(function(el) {
el.classList.remove('selected');
});
this.componentInstances.forEach(function(instance) {
if (instance.handlesContainer) {
instance.handlesContainer.style.display = 'none';
}
});
this.propertyPanelEl.innerHTML = '';
if (this.isMobile) {
this.mobilePanelMode = 'toolbox';
this._updateMobilePanelVisibility();
}
},
_updateMobilePanelVisibility: function() {
if (!this.isMobile) return;
if (this.mobilePanelMode === 'property') {
this.toolboxEl.classList.add('wb-mobile-hidden');
this.propertyPanelEl.classList.remove('wb-mobile-hidden');
} else {
this.toolboxEl.classList.remove('wb-mobile-hidden');
this.propertyPanelEl.classList.add('wb-mobile-hidden');
}
},
_showPropertyPanel: function(instance) {
var typeDef = this.componentTypes[instance.type];
var self = this;
var columns = this.phoneConfig.columns;
var startCol = instance.grid.startCell % columns;
var startRow = Math.floor(instance.grid.startCell / columns);
var html;
var deleteBtn;
var closeBtn;
html = '' + (typeDef.label || instance.type) + '
';
html += '';
if (typeDef.traits && typeDef.traits.length > 0) {
html += '';
}
html += '';
html += '';
html += '
';
this.propertyPanelEl.innerHTML = html;
this.propertyPanelEl.querySelectorAll('input').forEach(function(input) {
input.addEventListener('change', function() {
var prop = input.getAttribute('data-prop');
var trait = input.getAttribute('data-trait');
var value;
var currentRow;
var currentCol;
var typeDef2;
var traitDef;
var content;
var handlesContainer;
var sCol;
var sRow;
var cSpan;
if (prop) {
value = parseInt(input.value, 10) || 1;
if (prop === 'startCol') {
currentRow = Math.floor(instance.grid.startCell / columns);
instance.grid.startCell = currentRow * columns + (value - 1);
} else if (prop === 'startRow') {
currentCol = instance.grid.startCell % columns;
instance.grid.startCell = (value - 1) * columns + currentCol;
} else if (prop === 'colSpan') {
instance.grid.colSpan = value;
} else if (prop === 'rowSpan') {
instance.grid.rowSpan = value;
}
sCol = instance.grid.startCell % columns;
sRow = Math.floor(instance.grid.startCell / columns);
cSpan = Math.min(instance.grid.colSpan, columns - sCol);
instance.element.style.gridColumn = (sCol + 1) + ' / span ' + cSpan;
instance.element.style.gridRow = (sRow + 1) + ' / span ' + instance.grid.rowSpan;
} else if (trait) {
typeDef2 = self.componentTypes[instance.type];
traitDef = typeDef2.traits.find(function(t) { return t.name === trait; });
if (input.type === 'checkbox') {
instance.props[trait] = input.checked;
} else if (traitDef && traitDef.type === 'number') {
instance.props[trait] = parseFloat(input.value) || 0;
} else {
instance.props[trait] = input.value;
}
if (typeDef2.render) {
content = typeDef2.render(instance.props);
instance.element.innerHTML = '';
if (typeof content === 'string') {
instance.element.innerHTML = content;
} else if (content instanceof HTMLElement) {
instance.element.appendChild(content);
}
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';
handlesContainer.style.zIndex = '10';
instance.element.appendChild(handlesContainer);
instance.handlesContainer = handlesContainer;
self._recreateResizeHandles(instance);
}
}
self._notifyChanged('update', instance);
});
});
deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]');
if (deleteBtn) {
deleteBtn.addEventListener('click', function() {
self._deleteComponent(instance.id);
});
}
if (this.isMobile) {
closeBtn = document.createElement('button');
closeBtn.className = 'btn wb-mobile-close-panel';
closeBtn.textContent = '返回组件库';
closeBtn.addEventListener('click', function() {
self._deselectAll();
});
this.propertyPanelEl.appendChild(closeBtn);
}
},
_recreateResizeHandles: function(instance) {
var self = this;
var directions;
if (!instance || !instance.handlesContainer) return;
directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
instance.handlesContainer.innerHTML = '';
directions.forEach(function(direction) {
var handle = document.createElement('div');
handle.className = 'resize-handle resize-handle-' + direction;
handle.setAttribute('data-direction', direction);
self._applyHandleBaseStyle(handle);
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', function(e) {
e.stopPropagation();
e.preventDefault();
self._handleResizeStart(e, instance.id, direction);
});
instance.handlesContainer.appendChild(handle);
});
},
_deleteComponent: function(componentId) {
var index = -1;
var i;
var instance;
for (i = 0; i < this.componentInstances.length; i++) {
if (this.componentInstances[i].id === componentId) {
index = i;
break;
}
}
if (index === -1) return;
instance = this.componentInstances[index];
if (instance.element) {
instance.element.remove();
}
this.componentInstances.splice(index, 1);
this._deselectAll();
this._notifyChanged('delete', { id: componentId });
},
_findInstance: function(componentId) {
var i;
for (i = 0; i < this.componentInstances.length; i++) {
if (this.componentInstances[i].id === componentId) {
return this.componentInstances[i];
}
}
return null;
},
define: function(name, definition) {
if (this.componentTypes[name]) {
console.warn('Component type already defined:', name);
return;
}
this.componentTypes[name] = definition;
this._addToToolbox(name, definition);
},
_addToToolbox: function(name, definition) {
var self = this;
var item;
var icon;
var label;
item = document.createElement('div');
item.className = 'webbuilder-toolbox-item';
if (this.isMobile) {
item.classList.add('wb-mobile-toolbox-item');
}
item.setAttribute('draggable', true);
item.setAttribute('data-type', name);
icon = definition.icon || '\uD83D\uDCE6';
label = definition.label || name;
item.innerHTML = '' + icon + '' + label + '';
item.addEventListener('dragstart', function() {
self.draggingType = name;
item.style.opacity = '0.5';
});
item.addEventListener('dragend', function() {
item.style.opacity = '1';
self.draggingType = null;
});
if (this.isMobile) {
item.addEventListener('click', function() {
self.touchDragType = name;
});
}
this.toolboxListEl.appendChild(item);
},
toJSONB: function() {
var self = this;
var components;
components = this.componentInstances.map(function(instance) {
return {
id: instance.id,
type: instance.type,
grid: {
startCell: instance.grid.startCell,
colSpan: instance.grid.colSpan,
rowSpan: instance.grid.rowSpan
},
props: JSON.parse(JSON.stringify(instance.props))
};
});
return {
version: '1.0',
layouts: {
phone: {
canvas: {
width: this.phoneConfig.width,
height: this.phoneConfig.height,
columns: this.phoneConfig.columns,
cellSize: this.cellSize,
rows: this.rows,
gap: this.phoneConfig.gap,
background: this.phoneConfig.background
},
components: components
},
computer: null
}
};
},
fromJSONB: function(jsonb) {
var phoneLayout;
var self = this;
var maxId;
this.canvasEl.innerHTML = '';
this.componentInstances = [];
this.selectedComponent = null;
phoneLayout = jsonb.layouts && jsonb.layouts.phone;
if (!phoneLayout) {
console.warn('No phone layout found');
return;
}
if (phoneLayout.components) {
phoneLayout.components.forEach(function(comp) {
var typeDef = self.componentTypes[comp.type];
var instance;
if (!typeDef) {
console.warn('Component type not found:', comp.type);
return;
}
instance = {
id: comp.id,
type: comp.type,
grid: {
startCell: comp.grid.startCell,
colSpan: comp.grid.colSpan,
rowSpan: comp.grid.rowSpan
},
props: comp.props || {}
};
self._renderComponent(instance);
self.componentInstances.push(instance);
});
maxId = 0;
this.componentInstances.forEach(function(inst) {
var match = inst.id.match(/-(\d+)$/);
if (match) {
maxId = Math.max(maxId, parseInt(match[1], 10));
}
});
this.idCounter = maxId;
}
this._notifyChanged('load', { count: this.componentInstances.length });
},
getCanvasConfig: function() {
return {
width: this.phoneConfig.width,
height: this.phoneConfig.height,
columns: this.phoneConfig.columns,
cellSize: this.cellSize,
rows: this.rows,
gap: this.phoneConfig.gap,
background: this.phoneConfig.background
};
},
getComponents: function() {
return JSON.parse(JSON.stringify(this.componentInstances));
},
clear: function() {
this.canvasEl.innerHTML = '';
this.componentInstances = [];
this.selectedComponent = null;
this.propertyPanelEl.innerHTML = '';
this._notifyChanged('clear', null);
}
};