landestreffen-webseite/plugins/form/assets/filepond-handler.js

662 lines
No EOL
27 KiB
JavaScript

/**
* Unified Grav Form FilePond Handler
*
* This script initializes and configures FilePond instances for file uploads
* within Grav forms. It works with both normal and XHR form submissions.
* It also handles reinitializing FilePond instances after XHR form submissions.
*/
// Immediately-Invoked Function Expression for scoping
(function () {
// Check if script already loaded
if (window.gravFilepondHandlerLoaded) {
console.log('FilePond unified handler already loaded, skipping.');
return;
}
window.gravFilepondHandlerLoaded = true;
// Debugging - set to false for production
const debug = true;
// Helper function for logging
function log(message, type = 'log') {
if (!debug && type !== 'error') return;
const prefix = '[FilePond Handler]';
if (type === 'error') {
console.error(prefix, message);
} else if (type === 'warn') {
console.warn(prefix, message);
} else {
console.log(prefix, message);
}
}
// Track FilePond instances with their configuration
const pondInstances = new Map();
// Get translations from global object if available
const translations = window.GravForm?.translations?.PLUGIN_FORM || {
FILEPOND_REMOVE_FILE: 'Remove file',
FILEPOND_REMOVE_FILE_CONFIRMATION: 'Are you sure you want to remove this file?',
FILEPOND_CANCEL_UPLOAD: 'Cancel upload',
FILEPOND_ERROR_FILESIZE: 'File is too large',
FILEPOND_ERROR_FILETYPE: 'Invalid file type'
};
// Track initialization state
let initialized = false;
/**
* Get standard FilePond configuration for an element
* This is used for both initial setup and reinit after XHR
* @param {HTMLElement} element - The file input element
* @param {HTMLElement} container - The container element
* @returns {Object} Configuration object for FilePond
*/
function getFilepondConfig(element, container) {
if (!container) {
log('Container not provided for config extraction', 'error');
return null;
}
// Check if the field is required - this is correct location
const isRequired = element.hasAttribute('required') ||
container.hasAttribute('required') ||
container.getAttribute('data-required') === 'true';
// Then, add this code to remove the required attribute from the actual input
// to prevent browser validation errors, but keep track of the requirement
if (isRequired) {
// Store the required state on the container for our custom validation
container.setAttribute('data-required', 'true');
// Remove the required attribute from the input to avoid browser validation errors
element.removeAttribute('required');
}
try {
// Get settings from data attributes
const settingsAttr = container.getAttribute('data-grav-file-settings');
if (!settingsAttr) {
log('No file settings found for FilePond element', 'warn');
return null;
}
// Parse settings
let settings;
try {
settings = JSON.parse(settingsAttr);
log('Parsed settings:', settings);
} catch (e) {
log(`Error parsing file settings: ${e.message}`, 'error');
return null;
}
// Parse FilePond options
const filepondOptionsAttr = container.getAttribute('data-filepond-options') || '{}';
let filepondOptions;
try {
filepondOptions = JSON.parse(filepondOptionsAttr);
log('Parsed FilePond options:', filepondOptions);
} catch (e) {
log(`Error parsing FilePond options: ${e.message}`, 'error');
filepondOptions = {};
}
// Get URLs for upload and remove
const uploadUrl = container.getAttribute('data-file-url-add');
const removeUrl = container.getAttribute('data-file-url-remove');
if (!uploadUrl) {
log('Upload URL not found for FilePond element', 'warn');
return null;
}
// Parse previously uploaded files
const existingFiles = [];
const fileDataElements = container.querySelectorAll('[data-file]');
log(`Found ${fileDataElements.length} existing file data elements`);
fileDataElements.forEach(fileData => {
try {
const fileAttr = fileData.getAttribute('data-file');
log('File data attribute:', fileAttr);
const fileJson = JSON.parse(fileAttr);
if (fileJson && fileJson.name) {
existingFiles.push({
source: fileJson.name,
options: {
type: 'local',
file: {
name: fileJson.name,
size: fileJson.size,
type: fileJson.type
},
metadata: {
poster: fileJson.thumb_url || fileJson.path
}
}
});
}
} catch (e) {
log(`Error parsing file data: ${e.message}`, 'error');
}
});
log('Existing files:', existingFiles);
// Get form elements for Grav integration
const fieldName = container.getAttribute('data-file-field-name');
const form = element.closest('form');
const formNameInput = form ? form.querySelector('[name="__form-name__"]') : document.querySelector('[name="__form-name__"]');
const formIdInput = form ? form.querySelector('[name="__unique_form_id__"]') : document.querySelector('[name="__unique_form_id__"]');
const formNonceInput = form ? form.querySelector('[name="form-nonce"]') : document.querySelector('[name="form-nonce"]');
if (!formNameInput || !formIdInput || !formNonceInput) {
log('Missing required form inputs for proper Grav integration', 'warn');
}
// Configure FilePond
const options = {
// Core settings
name: settings.paramName,
maxFiles: settings.limit || null,
maxFileSize: `${settings.filesize}MB`,
acceptedFileTypes: settings.accept,
files: existingFiles,
// Server configuration - modified for Grav
server: {
process: {
url: uploadUrl,
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
ondata: (formData) => {
// Safety check - ensure formData is valid
if (!formData) {
console.error('FormData is undefined in ondata');
return new FormData(); // Return empty FormData as fallback
}
// Add all required Grav form fields
if (formNameInput) formData.append('__form-name__', formNameInput.value);
if (formIdInput) formData.append('__unique_form_id__', formIdInput.value);
formData.append('__form-file-uploader__', '1');
if (formNonceInput) formData.append('form-nonce', formNonceInput.value);
formData.append('task', 'filesupload');
// Use fieldName from the outer scope
if (fieldName) {
formData.append('name', fieldName);
} else {
console.error('Field name is undefined, falling back to default');
formData.append('name', 'files');
}
// Add URI if needed
const uriInput = document.querySelector('[name="uri"]');
if (uriInput) {
formData.append('uri', uriInput.value);
}
// Note: Don't try to append file here, FilePond will do that based on the name parameter
// Just return the modified formData
log('Prepared form data for Grav upload');
return formData;
}
},
revert: removeUrl ? {
url: removeUrl,
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
ondata: (formData, file) => {
// Add all required Grav form fields
if (formNameInput) formData.append('__form-name__', formNameInput.value);
if (formIdInput) formData.append('__unique_form_id__', formIdInput.value);
formData.append('__form-file-remover__', '1');
if (formNonceInput) formData.append('form-nonce', formNonceInput.value);
formData.append('name', fieldName);
// Add filename
formData.append('filename', file.filename);
log('Prepared form data for file removal');
return formData;
}
} : null
},
// Image Transform settings - both FilePond native settings and our custom ones
// Native settings
allowImagePreview: true,
allowImageResize: true,
allowImageTransform: true,
imagePreviewHeight: filepondOptions.imagePreviewHeight || 256,
// Transform settings
imageTransformOutputMimeType: filepondOptions.imageTransformOutputMimeType || 'image/jpeg',
imageTransformOutputQuality: filepondOptions.imageTransformOutputQuality || settings.resizeQuality || 90,
imageTransformOutputStripImageHead: filepondOptions.imageTransformOutputStripImageHead !== false,
// Resize settings
imageResizeTargetWidth: filepondOptions.imageResizeTargetWidth || settings.resizeWidth || null,
imageResizeTargetHeight: filepondOptions.imageResizeTargetHeight || settings.resizeHeight || null,
imageResizeMode: filepondOptions.imageResizeMode || 'cover',
imageResizeUpscale: filepondOptions.imageResizeUpscale || false,
// Crop settings
allowImageCrop: filepondOptions.allowImageCrop || false,
imageCropAspectRatio: filepondOptions.imageCropAspectRatio || null,
// Labels and translations
labelIdle: filepondOptions.labelIdle || '<span class="filepond--label-action">Browse</span> or drop files',
labelFileTypeNotAllowed: translations.FILEPOND_ERROR_FILETYPE || 'Invalid file type',
labelFileSizeNotAllowed: translations.FILEPOND_ERROR_FILESIZE || 'File is too large',
labelFileLoading: 'Loading',
labelFileProcessing: 'Uploading',
labelFileProcessingComplete: 'Upload complete',
labelFileProcessingAborted: 'Upload cancelled',
labelTapToCancel: translations.FILEPOND_CANCEL_UPLOAD || 'Cancel upload',
labelTapToRetry: 'Retry',
labelTapToUndo: 'Undo',
labelButtonRemoveItem: translations.FILEPOND_REMOVE_FILE || 'Remove',
// Style settings
stylePanelLayout: filepondOptions.stylePanelLayout || 'compact',
styleLoadIndicatorPosition: filepondOptions.styleLoadIndicatorPosition || 'center bottom',
styleProgressIndicatorPosition: filepondOptions.styleProgressIndicatorPosition || 'center bottom',
styleButtonRemoveItemPosition: filepondOptions.styleButtonRemoveItemPosition || 'right',
// Override with any remaining user-provided options
...filepondOptions
};
log('Prepared FilePond configuration:', options);
return options;
} catch (e) {
log(`Error creating FilePond configuration: ${e.message}`, 'error');
console.error(e); // Full error in console
return null;
}
}
/**
* Initialize a single FilePond instance
* @param {HTMLElement} element - The file input element to initialize
* @returns {FilePond|null} The created FilePond instance, or null if creation failed
*/
function initializeSingleFilePond(element) {
const container = element.closest('.filepond-root');
if (!container) {
log('FilePond container not found for input element', 'error');
return null;
}
// Don't initialize twice
if (container.classList.contains('filepond--hopper') || container.querySelector('.filepond--hopper')) {
log('FilePond already initialized for this element, skipping');
return null;
}
// Get the element ID or create a unique one for tracking
const elementId = element.id || `filepond-${Math.random().toString(36).substring(2, 15)}`;
// Get configuration
const config = getFilepondConfig(element, container);
if (!config) {
log('Failed to get configuration, cannot initialize FilePond', 'error');
return null;
}
log(`Initializing FilePond element ${elementId} with config`, config);
try {
// Create FilePond instance
const pond = FilePond.create(element, config);
log(`FilePond instance created successfully for element ${elementId}`);
// Store the instance and its configuration for potential reinit
pondInstances.set(elementId, {
instance: pond,
config: config,
container: container
});
// Add a reference to the element for easier lookup
element.filepondId = elementId;
container.filepondId = elementId;
// Handle form submission to ensure files are processed before submit
const form = element.closest('form');
if (form && !form._filepond_handler_attached) {
form._filepond_handler_attached = true;
form.addEventListener('submit', function (e) {
// Check for all FilePond instances in this form
const formPonds = Array.from(pondInstances.values())
.filter(info => info.instance && info.container.closest('form') === form);
const processingFiles = formPonds.reduce((total, info) => {
return total + info.instance.getFiles().filter(file =>
file.status === FilePond.FileStatus.PROCESSING_QUEUED ||
file.status === FilePond.FileStatus.PROCESSING
).length;
}, 0);
if (processingFiles > 0) {
e.preventDefault();
alert('Please wait for all files to finish uploading before submitting the form.');
return false;
}
});
}
return pond;
} catch (e) {
log(`Error creating FilePond instance: ${e.message}`, 'error');
console.error(e); // Full error in console
return null;
}
}
/**
* Main FilePond initialization function
* This will find and initialize all uninitialized FilePond elements
*/
function initializeFilePond() {
log('Starting FilePond initialization');
// Make sure we have the libraries loaded
if (typeof window.FilePond === 'undefined') {
log('FilePond library not found. Will retry in 500ms...', 'warn');
setTimeout(initializeFilePond, 500);
return;
}
log('FilePond library found, continuing initialization');
// Register plugins if available
try {
if (window.FilePondPluginFileValidateSize) {
FilePond.registerPlugin(FilePondPluginFileValidateSize);
log('Registered FileValidateSize plugin');
}
if (window.FilePondPluginFileValidateType) {
FilePond.registerPlugin(FilePondPluginFileValidateType);
log('Registered FileValidateType plugin');
}
if (window.FilePondPluginImagePreview) {
FilePond.registerPlugin(FilePondPluginImagePreview);
log('Registered ImagePreview plugin');
}
if (window.FilePondPluginImageResize) {
FilePond.registerPlugin(FilePondPluginImageResize);
log('Registered ImageResize plugin');
}
if (window.FilePondPluginImageTransform) {
FilePond.registerPlugin(FilePondPluginImageTransform);
log('Registered ImageTransform plugin');
}
} catch (e) {
log(`Error registering plugins: ${e.message}`, 'error');
}
// Find all FilePond elements
const elements = document.querySelectorAll('.filepond-root input[type="file"]:not(.filepond--browser)');
if (elements.length === 0) {
log('No FilePond form elements found on the page');
return;
}
log(`Found ${elements.length} FilePond element(s)`);
// Process each FilePond element
elements.forEach((element, index) => {
log(`Initializing FilePond element #${index + 1}`);
initializeSingleFilePond(element);
});
initialized = true;
log('FilePond initialization complete');
}
/**
* Reinitialize a specific FilePond instance
* @param {HTMLElement} container - The FilePond container element
* @returns {FilePond|null} The reinitialized FilePond instance, or null if reinitialization failed
*/
function reinitializeSingleFilePond(container) {
if (!container) {
log('No container provided for reinitialization', 'error');
return null;
}
// Check if this is a FilePond container
if (!container.classList.contains('filepond-root')) {
log('Container is not a FilePond container', 'warn');
return null;
}
log(`Reinitializing FilePond container: ${container.id || 'unnamed'}`);
// If already initialized, destroy first
if (container.classList.contains('filepond--hopper') || container.querySelector('.filepond--hopper')) {
log('Container already has an active FilePond instance, destroying it first');
// Try to find and destroy through our internal tracking
const elementId = container.filepondId;
if (elementId && pondInstances.has(elementId)) {
const info = pondInstances.get(elementId);
if (info.instance) {
log(`Destroying tracked FilePond instance for element ${elementId}`);
info.instance.destroy();
pondInstances.delete(elementId);
}
} else {
// Fallback: Try to find via child element with class
const pondElement = container.querySelector('.filepond--root');
if (pondElement && pondElement._pond) {
log('Destroying FilePond instance via DOM reference');
pondElement._pond.destroy();
}
}
}
// Look for the file input
const input = container.querySelector('input[type="file"]:not(.filepond--browser)');
if (!input) {
log('No file input found in container for reinitialization', 'error');
return null;
}
// Create a new instance
return initializeSingleFilePond(input);
}
/**
* Reinitialize all FilePond instances
* This is used after XHR form submissions
*/
function reinitializeFilePond() {
log('Reinitializing all FilePond instances');
// Find all FilePond containers
const containers = document.querySelectorAll('.filepond-root');
if (containers.length === 0) {
log('No FilePond containers found for reinitialization');
return;
}
log(`Found ${containers.length} FilePond container(s) for reinitialization`);
// Process each container
containers.forEach((container, index) => {
log(`Reinitializing FilePond container #${index + 1}`);
reinitializeSingleFilePond(container);
});
log('FilePond reinitialization complete');
}
/**
* Helper function to support XHR form interaction
* This hooks into the GravFormXHR system if available
*/
function setupXHRIntegration() {
// Only run if GravFormXHR is available
if (window.GravFormXHR) {
log('Setting up XHR integration for FilePond');
// Store original submit function
const originalSubmit = window.GravFormXHR.submit;
// Override to handle FilePond files
window.GravFormXHR.submit = function (form) {
if (!form) {
return originalSubmit.apply(this, arguments);
}
// Check for any FilePond instances in the form
let hasPendingUploads = false;
// First check via our tracking
Array.from(pondInstances.values()).forEach(info => {
if (info.container.closest('form') === form) {
const processingFiles = info.instance.getFiles().filter(file =>
file.status === FilePond.FileStatus.PROCESSING_QUEUED ||
file.status === FilePond.FileStatus.PROCESSING);
if (processingFiles.length > 0) {
hasPendingUploads = true;
}
}
});
// Fallback check for any untracked instances
if (!hasPendingUploads) {
const filepondContainers = form.querySelectorAll('.filepond-root');
filepondContainers.forEach(container => {
const pondElement = container.querySelector('.filepond--root');
if (pondElement && pondElement._pond) {
const pond = pondElement._pond;
const processingFiles = pond.getFiles().filter(file =>
file.status === FilePond.FileStatus.PROCESSING_QUEUED ||
file.status === FilePond.FileStatus.PROCESSING);
if (processingFiles.length > 0) {
hasPendingUploads = true;
}
}
});
}
if (hasPendingUploads) {
alert('Please wait for all files to finish uploading before submitting the form.');
return false;
}
// Call the original submit function
return originalSubmit.apply(this, arguments);
};
// Set up listeners for form updates
document.addEventListener('grav-form-updated', function (e) {
log('Detected form update event, reinitializing FilePond instances');
setTimeout(reinitializeFilePond, 100);
});
}
}
/**
* Setup mutation observer to detect dynamically added FilePond elements
*/
function setupMutationObserver() {
if (window.MutationObserver) {
const observer = new MutationObserver((mutations) => {
let shouldCheck = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
if (node.classList && node.classList.contains('filepond-root') ||
node.querySelector && node.querySelector('.filepond-root')) {
shouldCheck = true;
break;
}
}
}
}
if (shouldCheck) break;
}
if (shouldCheck) {
log('DOM changes detected that might include FilePond elements');
// Delay to ensure DOM is fully updated
setTimeout(initializeFilePond, 50);
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
log('MutationObserver set up for FilePond elements');
}
}
/**
* Initialize when DOM is ready
*/
function domReadyInit() {
log('DOM ready, initializing FilePond');
initializeFilePond();
setupXHRIntegration();
setupMutationObserver();
}
// Handle different document ready states
if (document.readyState === 'loading') {
log('Document still loading, adding DOMContentLoaded listener');
document.addEventListener('DOMContentLoaded', domReadyInit);
} else {
log('Document already loaded, initializing now');
setTimeout(domReadyInit, 0);
}
// Also support initialization via window load event as a fallback
window.addEventListener('load', function () {
log('Window load event fired');
if (!initialized) {
log('FilePond not yet initialized, initializing now');
initializeFilePond();
}
});
// Expose functions to global scope for external usage
window.GravFilePond = {
initialize: initializeFilePond,
reinitialize: reinitializeFilePond,
reinitializeContainer: reinitializeSingleFilePond,
getInstances: () => Array.from(pondInstances.values()).map(info => info.instance)
};
// Log initialization start
log('FilePond unified handler script loaded and ready');
})();