import { Controller } from '@hotwired/stimulus' // One product custom field of type `file` (see x-product-custom-fields). // Uploads the photo to the storefront's own endpoint (CustomFieldUploadController) // as soon as it's picked, then writes the returned opaque reference into the // hidden input the add-to-cart form actually submits — the checkout module // never receives the file itself. // // While uploading, the file input is marked invalid via setCustomValidity(), // so the browser's own form validation blocks add-to-cart until the reference // is in place. A failed upload clears the input, so `required` blocks it too. export default class extends Controller { static targets = ['file', 'reference', 'preview', 'error'] static values = { url: String, label: String, uploadingMessage: String, failedMessage: String, } disconnect() { this.abortController?.abort() this.revokePreview() } async upload() { this.reset() const file = this.fileTarget.files[0] if (!file) return const abortController = new AbortController() this.abortController = abortController this.fileTarget.setCustomValidity(this.uploadingMessageValue) this.fileTarget.setAttribute('aria-busy', 'true') const body = new FormData() body.append('file', file) body.append('label', this.labelValue) try { const response = await fetch(this.urlValue, { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '', 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json', }, body, signal: abortController.signal, }) const data = await response.json().catch(() => null) if (!response.ok || !data?.reference) { this.fail(data?.error) return } this.referenceTarget.value = data.reference this.showPreview(file) } catch (error) { // A newer pick superseded this upload — reset() already handled it. if (error.name === 'AbortError') return this.fail() } finally { if (!abortController.signal.aborted) this.markIdle() } } reset() { this.abortController?.abort() this.referenceTarget.value = '' this.errorTarget.hidden = true this.markIdle() this.revokePreview() } fail(message) { this.fileTarget.value = '' this.errorTarget.textContent = message || this.failedMessageValue this.errorTarget.hidden = false } markIdle() { this.fileTarget.setCustomValidity('') this.fileTarget.removeAttribute('aria-busy') } showPreview(file) { this.previewUrl = URL.createObjectURL(file) this.previewTarget.src = this.previewUrl this.previewTarget.hidden = false } // Formats the browser can't render (HEIC outside Safari) — the file // input's own filename is enough there. hidePreview() { this.previewTarget.hidden = true } revokePreview() { if (this.previewUrl) URL.revokeObjectURL(this.previewUrl) this.previewUrl = null this.previewTarget.removeAttribute('src') this.previewTarget.hidden = true } }