1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["iframe", "status"] 5 static values = { 6 url: String 7 } 8 9 connect() { 10 this.refreshTimer = null 11 this.abortController = null 12 this.requestSeq = 0 13 this.queueRefresh() 14 } 15 16 disconnect() { 17 this.abortInFlight() 18 if (this.refreshTimer) clearTimeout(this.refreshTimer) 19 } 20 21 queueRefresh(event) { 22 if (!this.shouldHandleEvent(event)) return 23 24 if (this.refreshTimer) clearTimeout(this.refreshTimer) 25 this.refreshTimer = window.setTimeout(() => this.refresh(), 350) 26 } 27 28 refreshNow(event) { 29 event?.preventDefault() 30 this.refresh() 31 } 32 33 async refresh() { 34 if (!this.hasUrlValue || !this.hasIframeTarget) return 35 36 this.abortInFlight() 37 this.abortController = new AbortController() 38 const requestId = ++this.requestSeq 39 40 this.setStatus("Updating preview...") 41 42 const payload = { 43 navigation_items: this.collectNavigationItems(), 44 site: this.collectSiteOptions() 45 } 46 47 try { 48 const response = await fetch(this.urlValue, { 49 method: "POST", 50 headers: { 51 "Accept": "text/html", 52 "Content-Type": "application/json", 53 "X-CSRF-Token": this.csrfToken() 54 }, 55 credentials: "same-origin", 56 signal: this.abortController.signal, 57 body: JSON.stringify(payload) 58 }) 59 60 if (!response.ok) { 61 throw new Error(`Preview failed (${response.status})`) 62 } 63 64 const html = await response.text() 65 if (requestId !== this.requestSeq) return 66 67 this.iframeTarget.srcdoc = html 68 this.setStatus("Preview updated") 69 } catch (error) { 70 if (error.name === "AbortError") return 71 this.setStatus("Preview unavailable") 72 } 73 } 74 75 collectNavigationItems() { 76 return Array.from(this.element.querySelectorAll("[data-nav-item]")).map((row) => { 77 const id = row.dataset.navItemId 78 // A group's rows nest inside its own, and so do the fields of the "add a 79 // link here" form, so read only the field set stamped with this id. 80 const scope = row.querySelector(`[data-nav-fields="${id}"]`) || row 81 const label = scope.querySelector("input[name='site_navigation_item[label]']")?.value.toString().trim() || "" 82 const kind = scope.querySelector("select[name='site_navigation_item[kind]']")?.value.toString() || "built_in" 83 const builtInKey = scope.querySelector("select[name='site_navigation_item[built_in_key]']")?.value.toString() || "" 84 const pageId = scope.querySelector("select[name='site_navigation_item[page_id]']")?.value.toString() || "" 85 const url = scope.querySelector("input[name='site_navigation_item[url]']")?.value.toString().trim() || "" 86 const visible = scope.querySelector("input[type='checkbox'][name='site_navigation_item[visible]']")?.checked || false 87 const parentId = scope.querySelector("select[name='site_navigation_item[parent_id]']")?.value.toString() || "" 88 89 return { 90 id, 91 label, 92 kind, 93 built_in_key: builtInKey, 94 page_id: pageId, 95 url, 96 visible, 97 parent_id: parentId 98 } 99 }) 100 } 101 102 collectSiteOptions() { 103 const showNameInput = this.element.querySelector("input[type='checkbox'][name='site[show_chamber_name_in_header]']") 104 if (!showNameInput) return {} 105 106 return { 107 show_chamber_name_in_header: showNameInput.checked 108 } 109 } 110 111 shouldHandleEvent(event) { 112 if (!event) return true 113 if (event.type === "site-navigation-menu:changed") return true 114 115 const target = event.target 116 if (!(target instanceof HTMLElement)) return false 117 if (target.closest("input[name='site[show_chamber_name_in_header]']")) return true 118 return Boolean(target.closest("[data-nav-item]")) 119 } 120 121 abortInFlight() { 122 if (this.abortController) this.abortController.abort() 123 this.abortController = null 124 } 125 126 setStatus(text) { 127 if (!this.hasStatusTarget) return 128 this.statusTarget.textContent = text 129 } 130 131 csrfToken() { 132 const meta = document.querySelector("meta[name='csrf-token']") 133 return meta ? meta.content : "" 134 } 135}
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.