1import { Controller } from "@hotwired/stimulus" 2 3// Orchestrator controller for the blocks builder 4// Holds shared state and coordinates between child controllers 5export default class extends Controller { 6 static targets = ["hiddenField", "blockCount"] 7 8 static values = { 9 apiUrl: String 10 } 11 12 static outlets = [ 13 "honey-block-builder--palette", 14 "honey-block-builder--canvas", 15 "honey-block-builder--config-panel", 16 "honey-block-builder--history", 17 "honey-block-builder--persistence" 18 ] 19 20 connect() { 21 this.blocks = JSON.parse(this.hiddenFieldTarget.value || "[]") 22 this.blockTypes = [] 23 this.assetOptions = {} 24 this.selectedBlockIndex = null 25 26 // Keyboard shortcuts 27 this.handleKeydown = this.handleKeydown.bind(this) 28 document.addEventListener("keydown", this.handleKeydown) 29 30 // Listen for events from child controllers 31 this.element.addEventListener("honey-block-builder--palette:request-add", this.handleBlockAdd.bind(this)) 32 this.element.addEventListener("honey-block-builder--canvas:request-insert", this.handleBlockInsert.bind(this)) 33 this.element.addEventListener("honey-block-builder--canvas:request-delete", this.handleBlockDelete.bind(this)) 34 this.element.addEventListener("honey-block-builder--canvas:request-reorder", this.handleBlockReorder.bind(this)) 35 this.element.addEventListener("honey-block-builder--canvas:request-select", this.handleBlockSelect.bind(this)) 36 this.element.addEventListener("honey-block-builder--canvas:request-duplicate", this.handleBlockDuplicate.bind(this)) 37 this.element.addEventListener("honey-block-builder--canvas:request-deselect", this.handleCanvasDeselect.bind(this)) 38 this.element.addEventListener("honey-block-builder--canvas:request-preview", this.handlePreviewRequest.bind(this)) 39 this.element.addEventListener("honey-block-builder--config-panel:request-config-update", this.handleConfigUpdate.bind(this)) 40 this.element.addEventListener("honey-block-builder--config-panel:request-duplicate-selected", this.handleDuplicateSelected.bind(this)) 41 this.element.addEventListener("honey-block-builder--config-panel:request-delete-selected", this.handleDeleteSelected.bind(this)) 42 this.element.addEventListener("honey-block-builder--config-panel:closed", this.handleConfigPanelClosed.bind(this)) 43 this.element.addEventListener("honey-block-builder--palette:drag-state-change", this.handleDragStateChange.bind(this)) 44 this.element.addEventListener("honey-block-builder--history:restore", this.handleHistoryRestore.bind(this)) 45 46 this.fetchBlockTypes() 47 } 48 49 disconnect() { 50 document.removeEventListener("keydown", this.handleKeydown) 51 } 52 53 handleKeydown(event) { 54 const isMod = event.metaKey || event.ctrlKey 55 56 if (isMod && event.key === "z" && !event.shiftKey) { 57 event.preventDefault() 58 this.requestUndo() 59 } else if (isMod && event.key === "z" && event.shiftKey) { 60 event.preventDefault() 61 this.requestRedo() 62 } else if (isMod && event.key === "y") { 63 event.preventDefault() 64 this.requestRedo() 65 } else if (event.key === "Escape" && this.selectedBlockIndex !== null) { 66 this.closeConfigPanel() 67 } 68 } 69 70 async fetchBlockTypes() { 71 try { 72 const response = await fetch(this.apiUrlValue, { 73 headers: { "Accept": "application/json" } 74 }) 75 const data = await response.json() 76 this.blockTypes = data.block_types 77 this.assetOptions = data.asset_options 78 79 // Notify child controllers that block types are ready 80 this.dispatch("block-types-loaded", { 81 detail: { blockTypes: this.blockTypes, assetOptions: this.assetOptions } 82 }) 83 84 // Initialize child controllers 85 this.initializeChildren() 86 } catch (error) { 87 console.error("Failed to fetch block types:", error) 88 } 89 } 90 91 initializeChildren() { 92 if (this.hasHoneyBlockBuilderPaletteOutlet) { 93 this.honeyBlockBuilderPaletteOutlet.setup(this.blockTypes) 94 } 95 if (this.hasHoneyBlockBuilderCanvasOutlet) { 96 this.honeyBlockBuilderCanvasOutlet.setup(this.blocks, this.blockTypes, this.assetOptions) 97 } 98 if (this.hasHoneyBlockBuilderPersistenceOutlet) { 99 this.honeyBlockBuilderPersistenceOutlet.setup(this.blocks) 100 } 101 } 102 103 // State accessors for child controllers 104 getBlocks() { 105 return this.blocks 106 } 107 108 getBlockTypes() { 109 return this.blockTypes 110 } 111 112 getAssetOptions() { 113 return this.assetOptions 114 } 115 116 getSelectedBlockIndex() { 117 return this.selectedBlockIndex 118 } 119 120 // Event handlers from child controllers 121 handleBlockAdd(event) { 122 const { blockType } = event.detail 123 this.insertBlockOfType(blockType, this.blocks.length) 124 } 125 126 handleBlockInsert(event) { 127 const { blockType, index } = event.detail 128 this.insertBlockOfType(blockType, index) 129 } 130 131 insertBlockOfType(blockType, insertIndex) { 132 const typeConfig = this.blockTypes.find(t => t.type === blockType) 133 if (!typeConfig) return 134 135 // Request history save before change 136 this.saveToHistory() 137 138 const newBlock = { 139 type: blockType, 140 config: {} 141 } 142 143 typeConfig.config.forEach(spec => { 144 if (spec.default !== null && spec.default !== undefined) { 145 newBlock.config[spec.name] = spec.default 146 } 147 }) 148 149 this.blocks.splice(insertIndex, 0, newBlock) 150 this.selectedBlockIndex = insertIndex 151 152 this.notifyBlocksChanged() 153 this.openConfigPanel() 154 } 155 156 handleBlockDelete(event) { 157 const { index } = event.detail 158 159 this.saveToHistory() 160 161 if (this.selectedBlockIndex === index) { 162 this.closeConfigPanel() 163 } else if (this.selectedBlockIndex > index) { 164 this.selectedBlockIndex-- 165 } 166 167 this.blocks.splice(index, 1) 168 this.notifyBlocksChanged() 169 } 170 171 handleBlockReorder(event) { 172 const { oldIndex, newIndex } = event.detail 173 174 this.saveToHistory() 175 176 const [moved] = this.blocks.splice(oldIndex, 1) 177 this.blocks.splice(newIndex, 0, moved) 178 179 // Update selected index if needed 180 if (this.selectedBlockIndex === oldIndex) { 181 this.selectedBlockIndex = newIndex 182 } else if (this.selectedBlockIndex > oldIndex && this.selectedBlockIndex <= newIndex) { 183 this.selectedBlockIndex-- 184 } else if (this.selectedBlockIndex < oldIndex && this.selectedBlockIndex >= newIndex) { 185 this.selectedBlockIndex++ 186 } 187 188 this.notifyBlocksChanged() 189 } 190 191 handleBlockSelect(event) { 192 const { index } = event.detail 193 194 if (this.selectedBlockIndex === index) { 195 this.closeConfigPanel() 196 } else { 197 this.selectedBlockIndex = index 198 this.openConfigPanel() 199 } 200 } 201 202 handleBlockDuplicate(event) { 203 this.duplicateBlockAt(event.detail.index) 204 } 205 206 handleDuplicateSelected() { 207 if (this.selectedBlockIndex === null) return 208 this.duplicateBlockAt(this.selectedBlockIndex) 209 } 210 211 duplicateBlockAt(index) { 212 const original = this.blocks[index] 213 if (!original) return 214 215 this.saveToHistory() 216 217 // Copy only type + config; host metadata keys (e.g. form_element_id) must 218 // not be carried over so the host treats the copy as a brand-new record. 219 const copy = { 220 type: original.type, 221 config: structuredClone(original.config || {}) 222 } 223 224 this.blocks.splice(index + 1, 0, copy) 225 this.selectedBlockIndex = index + 1 226 227 this.notifyBlocksChanged() 228 this.openConfigPanel() 229 } 230 231 handleCanvasDeselect() { 232 this.closeConfigPanel() 233 } 234 235 async handleDeleteSelected() { 236 if (this.selectedBlockIndex === null) return 237 238 const message = "Are you sure you want to delete this block?" 239 const confirmed = this.hasHoneyBlockBuilderCanvasOutlet 240 ? await this.honeyBlockBuilderCanvasOutlet.showConfirmDialog(message) 241 : confirm(message) 242 if (!confirmed) return 243 244 this.saveToHistory() 245 246 const index = this.selectedBlockIndex 247 this.closeConfigPanel() 248 this.blocks.splice(index, 1) 249 this.notifyBlocksChanged() 250 } 251 252 handleConfigUpdate(event) { 253 const { name, value } = event.detail 254 255 if (this.selectedBlockIndex === null) return 256 257 this.saveToHistory() 258 259 this.blocks[this.selectedBlockIndex].config[name] = value 260 this.notifyBlocksChanged() 261 } 262 263 handleDragStateChange(event) { 264 const { isDragging } = event.detail 265 if (this.hasHoneyBlockBuilderCanvasOutlet) { 266 if (isDragging) { 267 this.honeyBlockBuilderCanvasOutlet.showDropIndicators() 268 } else { 269 this.honeyBlockBuilderCanvasOutlet.hideDropIndicators() 270 } 271 } 272 } 273 274 handleHistoryRestore(event) { 275 const { blocks } = event.detail 276 this.blocks = blocks 277 this.closeConfigPanel() 278 this.notifyBlocksChanged() 279 } 280 281 // Flush any pending autosave so the persisted blocks are current, then ask 282 // the canvas to (re)load the server-rendered preview. 283 async handlePreviewRequest() { 284 if (this.hasHoneyBlockBuilderPersistenceOutlet) { 285 await this.honeyBlockBuilderPersistenceOutlet.autosave() 286 } 287 if (this.hasHoneyBlockBuilderCanvasOutlet) { 288 this.honeyBlockBuilderCanvasOutlet.loadPreview() 289 } 290 } 291 292 handleConfigPanelClosed() { 293 this.selectedBlockIndex = null 294 if (this.hasHoneyBlockBuilderCanvasOutlet) { 295 this.honeyBlockBuilderCanvasOutlet.clearHighlight() 296 } 297 } 298 299 // History helpers 300 saveToHistory() { 301 if (this.hasHoneyBlockBuilderHistoryOutlet) { 302 this.honeyBlockBuilderHistoryOutlet.saveState(this.blocks) 303 } 304 } 305 306 requestUndo() { 307 if (this.hasHoneyBlockBuilderHistoryOutlet) { 308 this.honeyBlockBuilderHistoryOutlet.undo(this.blocks) 309 } 310 } 311 312 requestRedo() { 313 if (this.hasHoneyBlockBuilderHistoryOutlet) { 314 this.honeyBlockBuilderHistoryOutlet.redo(this.blocks) 315 } 316 } 317 318 // Config panel helpers 319 openConfigPanel() { 320 if (this.selectedBlockIndex === null) return 321 if (!this.hasHoneyBlockBuilderConfigPanelOutlet) return 322 323 const block = this.blocks[this.selectedBlockIndex] 324 const typeConfig = this.blockTypes.find(t => t.type === block.type) 325 326 this.honeyBlockBuilderConfigPanelOutlet.open(block, typeConfig, this.assetOptions) 327 328 // Highlight selected block 329 if (this.hasHoneyBlockBuilderCanvasOutlet) { 330 this.honeyBlockBuilderCanvasOutlet.highlightBlock(this.selectedBlockIndex) 331 } 332 } 333 334 closeConfigPanel() { 335 // Just close the panel - handleConfigPanelClosed will handle cleanup 336 if (this.hasHoneyBlockBuilderConfigPanelOutlet) { 337 this.honeyBlockBuilderConfigPanelOutlet.close() 338 } 339 } 340 341 // Notify all children of blocks change 342 notifyBlocksChanged() { 343 this.syncToHiddenField() 344 this.updateBlockCount() 345 346 if (this.hasHoneyBlockBuilderCanvasOutlet) { 347 this.honeyBlockBuilderCanvasOutlet.render(this.blocks, this.blockTypes, this.assetOptions) 348 } 349 350 if (this.hasHoneyBlockBuilderPersistenceOutlet) { 351 this.honeyBlockBuilderPersistenceOutlet.markDirty(this.blocks) 352 } 353 354 this.dispatch("blocks-changed", { detail: { blocks: this.blocks } }) 355 } 356 357 syncToHiddenField() { 358 this.hiddenFieldTarget.value = JSON.stringify(this.blocks) 359 } 360 361 updateBlockCount() { 362 if (this.hasBlockCountTarget) { 363 this.blockCountTarget.textContent = this.blocks.length 364 } 365 } 366}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.