PageSourceSearch

https://www.georgetowncoc.com/assets/controllers/event_description_image_controller-d242c921.js

js georgetowncoc.com collected 2026-09-25 04:04:31 UTC 5,760 bytes, 183 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["editor", "status"]
5  static values = {
6    allowedTypes: Array,
7    maxBytes: Number,
8    uploadUrl: String
9  }
10
11  connect() {
12    this.pendingUploads = 0
13    this.activeRequests = new Set()
14    this.disabledSubmitters = new Set()
15    this.form = this.element.closest("form")
16    this.handleSubmit = this.preventSubmitWhileUploading.bind(this)
17    this.form?.addEventListener("submit", this.handleSubmit)
18    this.configureToolbar()
19  }
20
21  disconnect() {
22    this.form?.removeEventListener("submit", this.handleSubmit)
23    this.activeRequests.forEach((request) => request.abort())
24    this.enableSubmitters()
25  }
26
27  prepare(event) {
28    if (!this.hasEditorTarget || event.target !== this.editorTarget) return
29
30    this.configureToolbar(event.target)
31  }
32
33  accept(event) {
34    if (!this.hasEditorTarget || event.target !== this.editorTarget) return
35
36    event.stopPropagation()
37
38    const file = event.file
39    if (!file || !this.allowedTypesValue.includes(file.type)) {
40      event.preventDefault()
41      this.setStatus("Choose a JPG, PNG, WebP, or GIF image.", "error")
42      return
43    }
44
45    if (file.size > this.maxBytesValue) {
46      event.preventDefault()
47      this.setStatus("Choose an image smaller than 12 MB.", "error")
48    }
49  }
50
51  upload(event) {
52    if (!this.hasEditorTarget || event.target !== this.editorTarget) return
53
54    const attachment = event.attachment
55    if (!attachment?.file) return
56
57    // The app-wide Action Text handler would use the public direct-upload route.
58    // This editor uploads through the authenticated, tenant-scoped endpoint instead.
59    event.stopPropagation()
60    this.uploadAttachment(attachment)
61  }
62
63  configureToolbar(editor = this.hasEditorTarget ? this.editorTarget : null) {
64    const toolbarId = editor?.getAttribute("toolbar")
65    const toolbar = toolbarId ? document.getElementById(toolbarId) : null
66    if (!toolbar) return
67
68    toolbar.classList.add("event-description-image-toolbar")
69    const button = toolbar.querySelector("[data-trix-action='attachFiles']")
70    button?.setAttribute("title", "Add image")
71    button?.setAttribute("aria-label", "Add image")
72  }
73
74  uploadAttachment(attachment) {
75    const request = new XMLHttpRequest()
76    const formData = new FormData()
77    formData.append("file", attachment.file)
78
79    this.beginUpload(request)
80    attachment.setUploadProgress(1)
81    request.open("POST", this.uploadUrlValue)
82    request.setRequestHeader("Accept", "application/json")
83
84    const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
85    if (csrfToken) request.setRequestHeader("X-CSRF-Token", csrfToken)
86
87    request.upload.addEventListener("progress", (event) => {
88      if (!event.lengthComputable) return
89
90      attachment.setUploadProgress(Math.max(1, Math.round((event.loaded / event.total) * 95)))
91    })
92
93    request.addEventListener("load", () => {
94      const payload = this.responsePayload(request)
95
96      if (request.status >= 200 && request.status < 300 && payload.url) {
97        attachment.setAttributes({
98          url: payload.url,
99          eventDescriptionImageToken: payload.reference_token
100        })
101        attachment.setUploadProgress(100)
102        this.finishUpload(request, "Image added to the description.", "success")
103      } else {
104        this.failUpload(request, attachment, payload.error || "Upload failed.")
105      }
106    })
107    request.addEventListener("error", () => this.failUpload(request, attachment, "Upload failed. Check your connection and try again."))
108    request.addEventListener("abort", () => this.finishUpload(request))
109    request.send(formData)
110  }
111
112  beginUpload(request) {
113    this.activeRequests.add(request)
114    this.pendingUploads += 1
115    this.disableSubmitters()
116    this.setStatus(this.uploadingMessage(), "pending")
117  }
118
119  finishUpload(request, message = null, state = "pending") {
120    if (!this.activeRequests.delete(request)) return
121
122    this.pendingUploads = Math.max(0, this.pendingUploads - 1)
123    if (this.pendingUploads === 0) {
124      this.enableSubmitters()
125      if (message) this.setStatus(message, state)
126    } else {
127      this.setStatus(this.uploadingMessage(), "pending")
128    }
129  }
130
131  failUpload(request, attachment, message) {
132    attachment.remove()
133    this.finishUpload(request, message, "error")
134  }
135
136  preventSubmitWhileUploading(event) {
137    if (this.pendingUploads === 0) return
138
139    event.preventDefault()
140    this.setStatus("Wait for the image upload to finish before saving.", "error")
141  }
142
143  disableSubmitters() {
144    this.form?.querySelectorAll("button[type='submit'], input[type='submit']").forEach((submitter) => {
145      if (submitter.disabled) return
146
147      submitter.disabled = true
148      this.disabledSubmitters.add(submitter)
149    })
150  }
151
152  enableSubmitters() {
153    this.disabledSubmitters.forEach((submitter) => { submitter.disabled = false })
154    this.disabledSubmitters.clear()
155  }
156
157  uploadingMessage() {
158    return this.pendingUploads === 1 ? "Uploading image..." : `Uploading ${this.pendingUploads} images...`
159  }
160
161  responsePayload(request) {
162    try {
163      return JSON.parse(request.responseText || "{}")
164    } catch (_error) {
165      return {}
166    }
167  }
168
169  setStatus(message, state) {
170    if (!this.hasStatusTarget || !this.element.isConnected) return
171
172    this.statusTarget.textContent = message
173    this.statusTarget.classList.remove("text-zinc-500", "text-emerald-700", "text-rose-700")
174
175    if (state === "success") {
176      this.statusTarget.classList.add("text-emerald-700")
177    } else if (state === "error") {
178      this.statusTarget.classList.add("text-rose-700")
179    } else {
180      this.statusTarget.classList.add("text-zinc-500")
181    }
182  }
183}

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.