generated from boboko/starter
109 lines
3.4 KiB
JavaScript
109 lines
3.4 KiB
JavaScript
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
|
|
}
|
|
}
|