PageSourceSearch

https://www.georgetowncoc.com/assets/controllers/news_post_image_controller-1bada296.js

js georgetowncoc.com collected 2026-09-25 04:05:23 UTC 6,328 bytes, 206 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.uploadError = null
14    this.activeRequests = new Set()
15    this.disabledSubmitters = new Set()
16    this.form = this.hasEditorTarget ? this.editorTarget.closest("form") : this.element.closest("form")
17    this.handleSubmit = this.preventSubmitWhileUploading.bind(this)
18    this.form?.addEventListener("submit", this.handleSubmit)
19    this.configureToolbar()
20  }
21
22  disconnect() {
23    this.form?.removeEventListener("submit", this.handleSubmit)
24    this.activeRequests.forEach((request) => request.abort())
25    this.enableSubmitters()
26  }
27
28  prepare(event) {
29    if (!this.hasEditorTarget || event.target !== this.editorTarget) return
30
31    this.configureToolbar(event.target)
32  }
33
34  accept(event) {
35    if (!this.hasEditorTarget || event.target !== this.editorTarget) return
36
37    event.stopPropagation()
38
39    const file = event.file
40    if (!file || !this.allowedTypesValue.includes(file.type)) {
41      event.preventDefault()
42      this.setStatus("Choose a JPG, PNG, WebP, or GIF image.", "error")
43      return
44    }
45
46    if (file.size > this.maxBytesValue) {
47      event.preventDefault()
48      this.setStatus(`Choose an image smaller than ${this.maximumMegabytes()} MB.`, "error")
49    }
50  }
51
52  upload(event) {
53    if (!this.hasEditorTarget || event.target !== this.editorTarget) return
54
55    const attachment = event.attachment
56    if (!attachment?.file) return
57
58    event.stopPropagation()
59    this.uploadAttachment(attachment)
60  }
61
62  configureToolbar(editor = this.hasEditorTarget ? this.editorTarget : null) {
63    const toolbarId = editor?.getAttribute("toolbar")
64    const toolbar = toolbarId ? document.getElementById(toolbarId) : null
65    if (!toolbar) return
66
67    toolbar.classList.add("news-post-image-toolbar")
68    const button = toolbar.querySelector("[data-trix-action='attachFiles']")
69    button?.setAttribute("title", "Add image")
70    button?.setAttribute("aria-label", "Add image")
71  }
72
73  uploadAttachment(attachment) {
74    const request = new XMLHttpRequest()
75    const formData = new FormData()
76    formData.append("file", attachment.file)
77
78    this.beginUpload(request)
79    attachment.setUploadProgress(1)
80    request.open("POST", this.uploadUrlValue)
81    request.timeout = 120000
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          newsPostImageToken: payload.reference_token
100        })
101        attachment.setUploadProgress(100)
102        this.notifyEditorChanged()
103        this.finishUpload(request, "Image added to the article.", "success")
104      } else {
105        this.failUpload(request, attachment, payload.error || "Upload failed.")
106      }
107    })
108    request.addEventListener("error", () => {
109      this.failUpload(request, attachment, "Upload failed. Check your connection and try again.")
110    })
111    request.addEventListener("timeout", () => {
112      this.failUpload(request, attachment, "Upload timed out. Check your connection and try again.")
113    })
114    request.addEventListener("abort", () => this.finishUpload(request))
115    request.send(formData)
116  }
117
118  beginUpload(request) {
119    if (this.pendingUploads === 0) this.uploadError = null
120
121    this.activeRequests.add(request)
122    this.pendingUploads += 1
123    this.disableSubmitters()
124    this.setStatus(this.uploadingMessage(), "pending")
125  }
126
127  finishUpload(request, message = null, state = "pending") {
128    if (!this.activeRequests.delete(request)) return
129
130    this.pendingUploads = Math.max(0, this.pendingUploads - 1)
131    if (this.pendingUploads === 0) {
132      this.enableSubmitters()
133      if (this.uploadError) {
134        this.setStatus(this.uploadError, "error")
135      } else if (message) {
136        this.setStatus(message, state)
137      }
138    } else {
139      this.setStatus(this.uploadingMessage(), "pending")
140    }
141  }
142
143  failUpload(request, attachment, message) {
144    this.uploadError ||= message
145    attachment.remove()
146    this.finishUpload(request)
147  }
148
149  preventSubmitWhileUploading(event) {
150    if (this.pendingUploads === 0) return
151
152    event.preventDefault()
153    this.setStatus("Wait for the image upload to finish before saving.", "error")
154  }
155
156  disableSubmitters() {
157    this.form?.querySelectorAll("button[type='submit'], input[type='submit']").forEach((submitter) => {
158      if (submitter.disabled) return
159
160      submitter.disabled = true
161      this.disabledSubmitters.add(submitter)
162    })
163  }
164
165  enableSubmitters() {
166    this.disabledSubmitters.forEach((submitter) => { submitter.disabled = false })
167    this.disabledSubmitters.clear()
168  }
169
170  notifyEditorChanged() {
171    if (!this.hasEditorTarget) return
172
173    this.editorTarget.dispatchEvent(new Event("input", { bubbles: true }))
174  }
175
176  uploadingMessage() {
177    return this.pendingUploads === 1 ? "Uploading image..." : `Uploading ${this.pendingUploads} images...`
178  }
179
180  maximumMegabytes() {
181    return Math.floor(this.maxBytesValue / (1024 * 1024))
182  }
183
184  responsePayload(request) {
185    try {
186      return JSON.parse(request.responseText || "{}")
187    } catch (_error) {
188      return {}
189    }
190  }
191
192  setStatus(message, state) {
193    if (!this.hasStatusTarget || !this.element.isConnected) return
194
195    this.statusTarget.textContent = message
196    this.statusTarget.classList.remove("text-zinc-500", "text-emerald-700", "text-rose-700")
197
198    if (state === "success") {
199      this.statusTarget.classList.add("text-emerald-700")
200    } else if (state === "error") {
201      this.statusTarget.classList.add("text-rose-700")
202    } else {
203      this.statusTarget.classList.add("text-zinc-500")
204    }
205  }
206}

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.