plugins_Waivers_assets_js_controllers_waiver-upload-controller.js
import { Controller } from "@hotwired/stimulus"
/**
* Waiver Upload Controller
*
* Handles file selection, validation, preview, and upload progress for waiver images.
* Supports multiple file uploads with mobile camera capture integration.
*
* Targets:
* - waiverType: Waiver type select dropdown
* - fileInput: File input element
* - preview: Preview area container
* - progress: Progress bar container
* - progressBar: Progress bar element
* - progressText: Progress text element
* - submitButton: Submit button
*
* Actions:
* - handleFileSelect: Triggered when files are selected
* - handleSubmit: Triggered when form is submitted
*/
class WaiverUploadController extends Controller {
static targets = [
"waiverType",
"fileInput",
"preview",
"progress",
"progressBar",
"progressText",
"submitButton"
]
/**
* Maximum file size in bytes (25MB)
*/
static MAX_FILE_SIZE = 25 * 1024 * 1024
/**
* Allowed MIME types for uploads (images and PDFs)
*/
static ALLOWED_TYPES = [
'image/jpeg',
'image/jpg', // Some browsers/systems report JPEG as image/jpg
'image/png',
'image/gif',
'image/bmp',
'image/webp',
'image/x-ms-bmp', // Alternative MIME type for BMP
'image/x-windows-bmp', // Another BMP variant
'application/pdf' // PDF files
]
/**
* Initialize controller
*/
connect() {
console.log('WaiverUploadController connected')
this.selectedFiles = []
}
/**
* Handle file selection from input
*
* @param {Event} event File input change event
*/
handleFileSelect(event) {
const files = Array.from(event.target.files)
if (files.length === 0) {
return
}
// Validate files
const validationResults = files.map(file => this.validateFile(file))
const invalidFiles = validationResults.filter(result => !result.valid)
if (invalidFiles.length > 0) {
// Show error messages
const errors = invalidFiles.map(result => result.error).join('\n')
alert(`File validation errors:\n\n${errors}`)
}
// Filter to only valid files and append to existing selection
const validFiles = files.filter((file, index) =>
validationResults[index].valid
)
// Append new valid files to existing selection
this.selectedFiles = [...this.selectedFiles, ...validFiles]
// Create a new DataTransfer to update the file input with all selected files
const dataTransfer = new DataTransfer()
this.selectedFiles.forEach(file => {
dataTransfer.items.add(file)
})
this.fileInputTarget.files = dataTransfer.files
// Update preview
if (this.selectedFiles.length > 0) {
this.showPreview()
} else {
this.hidePreview()
}
}
/**
* Validate a single file
*
* @param {File} file File to validate
* @returns {Object} Validation result {valid: boolean, error: string}
*/
validateFile(file) {
// Check file size
if (file.size > WaiverUploadController.MAX_FILE_SIZE) {
return {
valid: false,
error: `${file.name}: File size (${this.formatFileSize(file.size)}) exceeds maximum of 25MB`
}
}
// Check file type (also allow .pdf extension as fallback)
if (!WaiverUploadController.ALLOWED_TYPES.includes(file.type) && !file.name.toLowerCase().endsWith('.pdf')) {
return {
valid: false,
error: `${file.name}: Invalid file type (${file.type}). Allowed: JPEG, PNG, GIF, BMP, WEBP, or PDF.`
}
}
return { valid: true }
}
/**
* Format file size for display
*
* @param {number} bytes File size in bytes
* @returns {string} Formatted file size
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
/**
* Show file preview area with selected files
*/
showPreview() {
if (!this.hasPreviewTarget) return
// Build preview HTML
const previewList = document.getElementById('file-preview-list')
if (!previewList) return
previewList.innerHTML = ''
this.selectedFiles.forEach((file, index) => {
const item = document.createElement('div')
item.className = 'list-group-item d-flex justify-content-between align-items-center'
item.innerHTML = `
<div>
<i class="bi bi-file-image text-primary"></i>
<strong>${this.escapeHtml(file.name)}</strong>
<br>
<small class="text-muted">${this.formatFileSize(file.size)}</small>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" data-index="${index}">
<i class="bi bi-x"></i>
</button>
`
// Add click handler to remove button
const removeBtn = item.querySelector('button')
removeBtn.addEventListener('click', () => this.removeFile(index))
previewList.appendChild(item)
})
// Show preview area
this.previewTarget.style.display = 'block'
}
/**
* Hide file preview area
*/
hidePreview() {
if (!this.hasPreviewTarget) return
this.previewTarget.style.display = 'none'
}
/**
* Remove a file from selection
*
* @param {number} index File index to remove
*/
removeFile(index) {
this.selectedFiles.splice(index, 1)
if (this.selectedFiles.length === 0) {
this.hidePreview()
this.fileInputTarget.value = ''
} else {
// Update file input with remaining files
const dataTransfer = new DataTransfer()
this.selectedFiles.forEach(file => {
dataTransfer.items.add(file)
})
this.fileInputTarget.files = dataTransfer.files
this.showPreview()
}
}
/**
* Handle form submission
*
* @param {Event} event Form submit event
*/
handleSubmit(event) {
// Validate waiver type is selected
if (!this.waiverTypeTarget.value) {
event.preventDefault()
alert('Please select a waiver type')
return
}
// Validate files are selected
if (this.selectedFiles.length === 0) {
event.preventDefault()
alert('Please select at least one image file to upload')
return
}
// Show progress bar
if (this.hasProgressTarget) {
this.progressTarget.style.display = 'block'
this.updateProgress(0)
}
// Disable submit button
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.disabled = true
this.submitButtonTarget.innerHTML = '<i class="bi bi-hourglass-split"></i> Uploading & Converting...'
}
// Form will submit normally - progress will be indeterminate
// since we're doing synchronous conversion on the server
this.simulateProgress()
}
/**
* Simulate upload progress (since conversion is synchronous)
*/
simulateProgress() {
let progress = 0
const interval = setInterval(() => {
progress += 5
if (progress >= 95) {
progress = 95 // Stop at 95% until server responds
clearInterval(interval)
}
this.updateProgress(progress)
}, 200)
}
/**
* Update progress bar
*
* @param {number} percent Progress percentage (0-100)
*/
updateProgress(percent) {
if (!this.hasProgressBarTarget || !this.hasProgressTextTarget) return
this.progressBarTarget.style.width = `${percent}%`
this.progressBarTarget.setAttribute('aria-valuenow', percent)
this.progressTextTarget.textContent = `${Math.round(percent)}%`
}
/**
* Escape HTML to prevent XSS
*
* @param {string} text Text to escape
* @returns {string} Escaped text
*/
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
// Add to global controllers registry
if (!window.Controllers) {
window.Controllers = {}
}
window.Controllers["waiver-upload"] = WaiverUploadController