PageSourceSearch

https://cdn2.futurefund.com/assets/honey/block_builder/controllers/builder_controller-bf9a5768.js

js futurefund.com collected 2026-10-01 15:13:10 UTC 11,138 bytes, 366 lines download raw bytes

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.