Files
3dealer/resources/js/stimulus/custom-field-upload-controller.js
T

109 lines
3.5 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 File row's id (boboko-core's
// Modules\Core\File\Models\File) 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 id 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?.file_id) {
this.fail(data?.error)
return
}
this.referenceTarget.value = data.file_id
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
}
}