PageSourceSearch

https://fgcchamber.org/assets/controllers/website_agent_pointer_controller-22c40a98.js

js fgcchamber.org collected 2026-10-07 02:05:25 UTC 7,502 bytes, 218 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// "Point to it": open the chamber's own website in a full-screen dialog, let them
4// click the section they mean, and attach that section to the request. Sections
5// are addressed through data-site-section-index, which the composed page
6// template emits for every section (see page_canvas_editor_controller.js).
7//
8// Only the page and the section's position travel with the request. The server
9// reads the section itself, so nothing the page displays is trusted.
10const SECTION_SELECTOR = "[data-site-section-index]"
11const CHROME_ATTRIBUTE = "data-ch-pointer"
12const MAX_POINTERS = 5
13
14export default class extends Controller {
15  static targets = ["dialog", "frame", "page", "chips", "chipTemplate"]
16  static values = { previewUrl: String, fieldName: String }
17
18  connect() {
19    this.onFrameLoad = () => this.attachFrame()
20  }
21
22  disconnect() {
23    this.detachFrame()
24  }
25
26  open() {
27    if (!this.hasDialogTarget) return
28    if (this.chipsTarget.children.length >= MAX_POINTERS) {
29      this.chipsTarget.querySelector("[data-role='remove']")?.focus()
30      return
31    }
32
33    this.dialogTarget.showModal()
34    this.loadPage(this.pageTarget.value)
35  }
36
37  close() {
38    if (this.hasDialogTarget && this.dialogTarget.open) this.dialogTarget.close()
39  }
40
41  closed() {
42    this.detachFrame()
43    this.frameTarget.removeAttribute("src")
44  }
45
46  changePage() {
47    this.loadPage(this.pageTarget.value)
48  }
49
50  loadPage(slug) {
51    const url = new URL(this.previewUrlValue, window.location.origin)
52    url.searchParams.set("page", slug || "home")
53    this.detachFrame()
54    this.frameTarget.addEventListener("load", this.onFrameLoad, { once: true })
55    this.frameTarget.src = url.toString()
56  }
57
58  remove(event) {
59    event.currentTarget.closest("li")?.remove()
60    this.element.querySelector("[data-action~='website-agent-pointer#open']")?.focus()
61  }
62
63  // Frame chrome ----------------------------------------------------------
64
65  attachFrame() {
66    const doc = this.frameDocument()
67    if (!doc?.body) return
68
69    this.doc = doc
70    const style = doc.createElement("style")
71    style.setAttribute(CHROME_ATTRIBUTE, "")
72    style.textContent = `
73      [${CHROME_ATTRIBUTE}-box] { position: absolute; z-index: 2147483646; pointer-events: none;
74        border: 3px solid #d97706; border-radius: 6px; background: rgba(245, 158, 11, 0.08);
75        box-shadow: 0 0 0 9999px rgba(24, 24, 27, 0.18); transition: all 80ms ease-out; }
76      [${CHROME_ATTRIBUTE}-label] { position: absolute; top: -30px; left: -3px; white-space: nowrap;
77        border-radius: 6px; background: #18181b; color: #fff; padding: 4px 8px;
78        font: 600 12px/16px system-ui, sans-serif; }
79      ${SECTION_SELECTOR} { cursor: pointer; }
80      ${SECTION_SELECTOR}:focus { outline: none; }
81    `
82    doc.head?.appendChild(style)
83
84    this.box = doc.createElement("div")
85    this.box.setAttribute(`${CHROME_ATTRIBUTE}-box`, "")
86    this.box.hidden = true
87    this.boxLabel = doc.createElement("span")
88    this.boxLabel.setAttribute(`${CHROME_ATTRIBUTE}-label`, "")
89    this.box.appendChild(this.boxLabel)
90    doc.body.appendChild(this.box)
91
92    this.sections().forEach((section) => {
93      if (!section.hasAttribute("tabindex")) section.setAttribute("tabindex", "0")
94    })
95
96    this.handlers = {
97      move: (event) => this.highlight(this.sectionFor(event.target)),
98      focus: (event) => this.highlight(this.sectionFor(event.target)),
99      click: (event) => this.pick(event),
100      key: (event) => this.keyPick(event),
101      submit: (event) => event.preventDefault(),
102    }
103    doc.addEventListener("mousemove", this.handlers.move, true)
104    doc.addEventListener("focusin", this.handlers.focus, true)
105    doc.addEventListener("click", this.handlers.click, true)
106    doc.addEventListener("keydown", this.handlers.key, true)
107    doc.addEventListener("submit", this.handlers.submit, true)
108
109    this.sections()[0]?.focus({ preventScroll: true })
110  }
111
112  detachFrame() {
113    if (this.doc && this.handlers) {
114      this.doc.removeEventListener("mousemove", this.handlers.move, true)
115      this.doc.removeEventListener("focusin", this.handlers.focus, true)
116      this.doc.removeEventListener("click", this.handlers.click, true)
117      this.doc.removeEventListener("keydown", this.handlers.key, true)
118      this.doc.removeEventListener("submit", this.handlers.submit, true)
119    }
120    if (this.hasFrameTarget) this.frameTarget.removeEventListener("load", this.onFrameLoad)
121    this.doc = null
122    this.handlers = null
123    this.box = null
124  }
125
126  highlight(section) {
127    if (!this.box) return
128    if (!section) {
129      this.box.hidden = true
130      return
131    }
132
133    const view = this.doc.defaultView
134    const rect = section.getBoundingClientRect()
135    this.box.hidden = false
136    this.box.style.left = `${Math.round(rect.left + view.scrollX)}px`
137    this.box.style.top = `${Math.round(rect.top + view.scrollY)}px`
138    this.box.style.width = `${Math.round(rect.width)}px`
139    this.box.style.height = `${Math.round(rect.height)}px`
140    this.boxLabel.textContent = `Click to choose: ${this.describe(section)}`
141    this.boxLabel.style.top = rect.top < 34 ? "4px" : "-30px"
142    this.boxLabel.style.left = rect.top < 34 ? "4px" : "-3px"
143  }
144
145  pick(event) {
146    event.preventDefault()
147    event.stopPropagation()
148    this.choose(this.sectionFor(event.target))
149  }
150
151  keyPick(event) {
152    if (event.key === "Escape") {
153      event.preventDefault()
154      this.close()
155      return
156    }
157    if (event.key !== "Enter" && event.key !== " ") return
158
159    const section = this.sectionFor(this.doc?.activeElement)
160    if (!section) return
161
162    event.preventDefault()
163    event.stopPropagation()
164    this.choose(section)
165  }
166
167  choose(section) {
168    if (!section) return
169
170    const index = Number(section.dataset.siteSectionIndex)
171    if (!Number.isInteger(index) || index < 0) return
172
173    const slug = this.pageTarget.value
174    const pageLabel = this.pageTarget.selectedOptions[0]?.textContent?.trim() || "Page"
175    const value = JSON.stringify({ page: slug, index })
176    const duplicate = Array.from(this.chipsTarget.querySelectorAll("[data-role='value']"))
177      .some((input) => input.value === value)
178    if (!duplicate) this.addChip(`${pageLabel}: ${this.describe(section)}`, value)
179
180    this.close()
181  }
182
183  addChip(label, value) {
184    const fragment = this.chipTemplateTarget.content.cloneNode(true)
185    const chip = fragment.firstElementChild
186    chip.querySelector("[data-role='label']").textContent = label
187    chip.querySelector("[data-role='label']").title = label
188    const input = chip.querySelector("[data-role='value']")
189    input.name = this.fieldNameValue
190    input.value = value
191    chip.querySelector("[data-role='remove']").setAttribute("aria-label", `Remove ${label}`)
192    this.chipsTarget.appendChild(chip)
193  }
194
195  // A heading says which section it is better than its position does.
196  describe(section) {
197    const heading = section.querySelector("h1, h2, h3, h4")?.textContent?.replace(/\s+/g, " ").trim()
198    if (heading) return `“${heading.length > 60 ? `${heading.slice(0, 59)}…` : heading}”`
199
200    return `section ${Number(section.dataset.siteSectionIndex) + 1}`
201  }
202
203  sections() {
204    return this.doc ? Array.from(this.doc.querySelectorAll(SECTION_SELECTOR)) : []
205  }
206
207  sectionFor(node) {
208    return node?.closest?.(SECTION_SELECTOR) || null
209  }
210
211  frameDocument() {
212    try {
213      return this.frameTarget.contentDocument
214    } catch (_error) {
215      return null
216    }
217  }
218}

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.