|
1 | 1 | import { Controller } from '@hotwired/stimulus' |
| 2 | +import { DirectUpload } from '@rails/activestorage' |
2 | 3 |
|
3 | 4 | // Connects to data-controller='ui--forms--file-upload' |
4 | 5 | // Shows the selected filename (or a count for multiple files) in the field, and |
5 | 6 | // frames the controls as a drop target while a file is dragged over the page. |
6 | 7 | export default class extends Controller { |
7 | | - static targets = ['input', 'filename', 'dropZone'] |
8 | | - static values = { placeholder: String } |
| 8 | + static targets = ['input', 'filename', 'dropZone', 'signedId'] |
| 9 | + static values = { placeholder: String, url: String, uploading: String, failed: String } |
| 10 | + |
| 11 | + connect () { |
| 12 | + this.boundHold = this.hold.bind(this) |
| 13 | + this.form?.addEventListener('submit', this.boundHold) |
| 14 | + } |
| 15 | + |
| 16 | + disconnect () { |
| 17 | + this.form?.removeEventListener('submit', this.boundHold) |
| 18 | + } |
| 19 | + |
| 20 | + get form () { |
| 21 | + return this.element.closest('form') |
| 22 | + } |
9 | 23 |
|
10 | 24 | // Both buttons open the one input; `capture` is what sends it to the camera. |
11 | 25 | takePicture () { |
@@ -69,6 +83,43 @@ export default class extends Controller { |
69 | 83 | files.length === 0 |
70 | 84 | ? this.placeholderValue |
71 | 85 | : files.length === 1 ? files[0].name : `${files.length} files` |
| 86 | + if (this.urlValue) this.upload(files[0]) |
| 87 | + } |
| 88 | + |
| 89 | + // Only when the field is nameless (direct_upload) - the form then carries the blob's |
| 90 | + // signed id rather than the bytes. |
| 91 | + upload (file) { |
| 92 | + if (!file) return |
| 93 | + |
| 94 | + this.xhr?.abort() // Picking again shouldn't leave the discarded file uploading |
| 95 | + this.signedIdTarget.value = '' |
| 96 | + this.filenameTarget.textContent = `${file.name} — ${this.uploadingValue}` |
| 97 | + |
| 98 | + const upload = new DirectUpload(file, this.urlValue, this) |
| 99 | + this.currentUpload = upload |
| 100 | + this.pending = new Promise((resolve) => upload.create((error, blob) => { |
| 101 | + resolve() |
| 102 | + if (this.currentUpload !== upload) return // A newer pick owns the field now |
| 103 | + |
| 104 | + this.pending = null |
| 105 | + if (!error) this.signedIdTarget.value = blob.signed_id |
| 106 | + this.filenameTarget.textContent = error ? `${file.name} — ${this.failedValue}` : file.name |
| 107 | + })) |
| 108 | + } |
| 109 | + |
| 110 | + // DirectUpload delegate hook - the handle that makes a discarded upload cancellable |
| 111 | + directUploadWillStoreFileWithXHR (xhr) { |
| 112 | + this.xhr = xhr |
| 113 | + } |
| 114 | + |
| 115 | + // Submitting mid-upload would drop the file, so hold the form until the blob lands. |
| 116 | + // A failed upload submits anyway - the rest of the form matters more. |
| 117 | + async hold (event) { |
| 118 | + if (!this.pending) return |
| 119 | + |
| 120 | + event.preventDefault() |
| 121 | + await this.pending |
| 122 | + this.form.requestSubmit() |
72 | 123 | } |
73 | 124 | } |
74 | 125 |
|
|
0 commit comments