Add premium logo and professional theme for high-end clients

- Create custom SVG logo with professional branding
- Implement premium color scheme with blue and gold accents
- Add custom CSS with professional styling for cards, tables, buttons
- Update logo template to use new logo.svg file
- Create custom favicon for complete branding
- Redesign homepage with premium content sections
- Update resources page with membership tiers and premium pricing
- Enhance contact page with testimonials and detailed information
- Target audience: high-paying clients ($100+/hour)
- Professional yet approachable design language

💘 Generated with Crush

Assisted-by: GLM-4.7 via Crush <crush@charm.land>
This commit is contained in:
Charles N Wyble
2026-01-13 16:15:40 -05:00
parent e6c15cafb3
commit 9f7fe553dc
2596 changed files with 433475 additions and 113 deletions

View File

@@ -0,0 +1,128 @@
(function() {
'use strict';
// Function to refresh a captcha image
const refreshCaptchaImage = function(container) {
const img = container.querySelector('img');
if (!img) {
console.warn('Cannot find captcha image in container');
return;
}
// Get the base URL and field ID
const baseUrl = img.dataset.baseUrl || img.src.split('?')[0];
const fieldId = img.dataset.fieldId || container.dataset.fieldId;
// Force reload by adding/updating timestamp and field ID
const timestamp = new Date().getTime();
let newUrl = baseUrl + '?t=' + timestamp;
if (fieldId) {
newUrl += '&field=' + fieldId;
}
img.src = newUrl;
// Also clear the input field if we can find it
const formField = container.closest('.form-field');
if (formField) {
const input = formField.querySelector('input[type="text"]');
if (input) {
input.value = '';
// Try to focus the input
try { input.focus(); } catch(e) {}
}
}
};
// Function to set up click handlers for refresh buttons
const setupRefreshButtons = function() {
// Find all captcha containers
const containers = document.querySelectorAll('[data-captcha-provider="basic-captcha"]');
containers.forEach(function(container) {
// Find the refresh button within this container
const button = container.querySelector('button');
if (!button) {
return;
}
// Remove any existing listeners (just in case)
button.removeEventListener('click', handleRefreshClick);
// Add the click handler
button.addEventListener('click', handleRefreshClick);
});
};
// Click handler function
const handleRefreshClick = function(event) {
// Prevent default behavior and stop propagation
event.preventDefault();
event.stopPropagation();
// Find the container
const container = this.closest('[data-captcha-provider="basic-captcha"]');
if (!container) {
return false;
}
// Refresh the image
refreshCaptchaImage(container);
return false;
};
// Set up a mutation observer to handle dynamically added captchas
const setupMutationObserver = function() {
// Check if MutationObserver is available
if (typeof MutationObserver === 'undefined') return;
// Create a mutation observer to watch for new captcha elements
const observer = new MutationObserver(function(mutations) {
let needsSetup = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length) {
// Check if any of the added nodes contain our captcha containers
for (let i = 0; i < mutation.addedNodes.length; i++) {
const node = mutation.addedNodes[i];
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if this element has or contains captcha containers
if (node.querySelector && (
node.matches('[data-captcha-provider="basic-captcha"]') ||
node.querySelector('[data-captcha-provider="basic-captcha"]')
)) {
needsSetup = true;
break;
}
}
}
}
});
if (needsSetup) {
setupRefreshButtons();
}
});
// Start observing the document
observer.observe(document.body, {
childList: true,
subtree: true
});
};
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', function() {
setupRefreshButtons();
setupMutationObserver();
// Also connect to XHR system if available (for best of both worlds)
if (window.GravFormXHR && window.GravFormXHR.captcha) {
window.GravFormXHR.captcha.register('basic-captcha', {
reset: function(container, form) {
refreshCaptchaImage(container);
}
});
}
});
})();

View File

@@ -0,0 +1,166 @@
(function() {
'use strict';
// Register the handler with the form system when it's ready
const registerRecaptchaHandler = function() {
if (window.GravFormXHR && window.GravFormXHR.captcha) {
window.GravFormXHR.captcha.register('recaptcha', {
reset: function(container, form) {
if (!form || !form.id) {
console.warn('Cannot reset reCAPTCHA: form is invalid or missing ID');
return;
}
const formId = form.id;
console.log(`Attempting to reset reCAPTCHA for form: ${formId}`);
// First try the expected ID pattern from the Twig template
const recaptchaId = `g-recaptcha-${formId}`;
// We need to look more flexibly for the container
let widgetContainer = document.getElementById(recaptchaId);
// If not found by ID, look for the div inside the captcha provider container
if (!widgetContainer) {
// Try to find it inside the captcha provider container
widgetContainer = container.querySelector('.g-recaptcha');
if (!widgetContainer) {
// If that fails, look more broadly in the form
widgetContainer = form.querySelector('.g-recaptcha');
if (!widgetContainer) {
// Last resort - create a new container if needed
console.warn(`reCAPTCHA container #${recaptchaId} not found. Creating a new one.`);
widgetContainer = document.createElement('div');
widgetContainer.id = recaptchaId;
widgetContainer.className = 'g-recaptcha';
container.appendChild(widgetContainer);
}
}
}
console.log(`Found reCAPTCHA container for form: ${formId}`);
// Get configuration from data attributes
const parentContainer = container.closest('[data-captcha-provider="recaptcha"]');
if (!parentContainer) {
console.warn('Cannot find reCAPTCHA parent container with data-captcha-provider attribute.');
return;
}
const sitekey = parentContainer.dataset.sitekey;
const version = parentContainer.dataset.version || '2-checkbox';
const isV3 = version.startsWith('3');
const isInvisible = version === '2-invisible';
if (!sitekey) {
console.warn('Cannot reinitialize reCAPTCHA - missing sitekey attribute');
return;
}
console.log(`Re-rendering reCAPTCHA widget for form: ${formId}, version: ${version}`);
// Handle V3 reCAPTCHA differently
if (isV3) {
try {
// For v3, we don't need to reset anything visible, just make sure we have the API
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.execute === 'function') {
// Create a new execution context for the form
const actionName = `form_${formId}`;
const tokenInput = form.querySelector('input[name="token"]') ||
form.querySelector('input[name="data[token]"]');
const actionInput = form.querySelector('input[name="action"]') ||
form.querySelector('input[name="data[action]"]');
if (tokenInput && actionInput) {
// Clear previous token
tokenInput.value = '';
// Set the action name
actionInput.value = actionName;
console.log(`reCAPTCHA v3 ready for execution on form: ${formId}`);
} else {
console.warn(`Cannot find token or action inputs for reCAPTCHA v3 in form: ${formId}`);
}
} else {
console.warn('reCAPTCHA v3 API not properly loaded.');
}
} catch (e) {
console.error(`Error setting up reCAPTCHA v3: ${e.message}`);
}
return;
}
// For v2, handle visible widget reset
// Clear the container to ensure fresh rendering
widgetContainer.innerHTML = '';
// Check if reCAPTCHA API is available
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.render === 'function') {
try {
// Render with a slight delay to ensure DOM is settled
setTimeout(() => {
grecaptcha.render(widgetContainer.id || widgetContainer, {
'sitekey': sitekey,
'theme': parentContainer.dataset.theme || 'light',
'size': isInvisible ? 'invisible' : 'normal',
'callback': function(token) {
console.log(`reCAPTCHA verification completed for form: ${formId}`);
// If it's invisible reCAPTCHA, submit the form automatically
if (isInvisible && window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
window.GravFormXHR.submit(form);
}
}
});
console.log(`Successfully rendered reCAPTCHA for form: ${formId}`);
}, 100);
} catch (e) {
console.error(`Error rendering reCAPTCHA widget: ${e.message}`);
widgetContainer.innerHTML = '<p style="color:red;">Error initializing reCAPTCHA.</p>';
}
} else {
console.warn('reCAPTCHA API not available. Attempting to reload...');
// Remove existing script if any
const existingScript = document.querySelector('script[src*="google.com/recaptcha/api.js"]');
if (existingScript) {
existingScript.parentNode.removeChild(existingScript);
}
// Create new script element
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/api.js${isV3 ? '?render=' + sitekey : ''}`;
script.async = true;
script.defer = true;
script.onload = function() {
console.log('reCAPTCHA API loaded, retrying widget render...');
setTimeout(() => {
const retryContainer = document.querySelector(`[data-captcha-provider="recaptcha"]`);
if (retryContainer && form) {
window.GravFormXHR.captcha.getProvider('recaptcha').reset(retryContainer, form);
}
}, 200);
};
document.head.appendChild(script);
}
}
});
console.log('reCAPTCHA XHR handler registered successfully');
} else {
console.error('GravFormXHR.captcha not found. Make sure the Form plugin is loaded correctly.');
}
};
// Try to register the handler immediately if GravFormXHR is already available
if (window.GravFormXHR && window.GravFormXHR.captcha) {
registerRecaptchaHandler();
} else {
// Otherwise, wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Give a small delay to ensure GravFormXHR is initialized
setTimeout(registerRecaptchaHandler, 100);
});
}
})();

View File

@@ -0,0 +1,121 @@
(function() {
'use strict';
// Register the handler with the form system when it's ready
const registerTurnstileHandler = function() {
if (window.GravFormXHR && window.GravFormXHR.captcha) {
window.GravFormXHR.captcha.register('turnstile', {
reset: function(container, form) {
const formId = form.id;
const containerId = `cf-turnstile-${formId}`;
const widgetContainer = document.getElementById(containerId);
if (!widgetContainer) {
console.warn(`Turnstile container #${containerId} not found.`);
return;
}
// Get configuration from data attributes
const parentContainer = container.closest('[data-captcha-provider="turnstile"]');
const sitekey = parentContainer ? parentContainer.dataset.sitekey : null;
if (!sitekey) {
console.warn('Cannot reinitialize Turnstile - missing sitekey attribute');
return;
}
// Clear the container to ensure fresh rendering
widgetContainer.innerHTML = '';
console.log(`Re-rendering Turnstile widget for form: ${formId}`);
// Check if Turnstile API is available
if (typeof window.turnstile !== 'undefined') {
try {
// Reset any existing widgets
try {
window.turnstile.reset(containerId);
} catch (e) {
// Ignore reset errors, we'll re-render anyway
}
// Render with a slight delay to ensure DOM is settled
setTimeout(() => {
window.turnstile.render(`#${containerId}`, {
sitekey: sitekey,
theme: parentContainer ? (parentContainer.dataset.theme || 'light') : 'light',
callback: function(token) {
console.log(`Turnstile verification completed for form: ${formId} with token:`, token.substring(0, 10) + '...');
// Create or update hidden input for token
let tokenInput = form.querySelector('input[name="cf-turnstile-response"]');
if (!tokenInput) {
console.log('Creating new hidden input for Turnstile token');
tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = 'cf-turnstile-response';
form.appendChild(tokenInput);
} else {
console.log('Updating existing hidden input for Turnstile token');
}
tokenInput.value = token;
// Also add a debug attribute
form.setAttribute('data-turnstile-verified', 'true');
},
'expired-callback': function() {
console.log(`Turnstile token expired for form: ${formId}`);
},
'error-callback': function(error) {
console.error(`Turnstile error for form ${formId}: ${error}`);
}
});
}, 100);
} catch (e) {
console.error(`Error rendering Turnstile widget: ${e.message}`);
widgetContainer.innerHTML = '<p style="color:red;">Error initializing Turnstile.</p>';
}
} else {
console.warn('Turnstile API not available. Attempting to reload...');
// Remove existing script if any
const existingScript = document.querySelector('script[src*="challenges.cloudflare.com/turnstile/v0/api.js"]');
if (existingScript) {
existingScript.parentNode.removeChild(existingScript);
}
// Create new script element
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
script.async = true;
script.defer = true;
script.onload = function() {
console.log('Turnstile API loaded, retrying widget render...');
setTimeout(() => {
const retryContainer = document.querySelector('[data-captcha-provider="turnstile"]');
if (retryContainer && form) {
window.GravFormXHR.captcha.getProvider('turnstile').reset(retryContainer, form);
}
}, 200);
};
document.head.appendChild(script);
}
}
});
console.log('Turnstile XHR handler registered successfully');
} else {
console.error('GravFormXHR.captcha not found. Make sure the Form plugin is loaded correctly.');
}
};
// Try to register the handler immediately if GravFormXHR is already available
if (window.GravFormXHR && window.GravFormXHR.captcha) {
registerTurnstileHandler();
} else {
// Otherwise, wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Give a small delay to ensure GravFormXHR is initialized
setTimeout(registerTurnstileHandler, 100);
});
}
})();

View File

@@ -0,0 +1,311 @@
/**
* Direct Dropzone Initialization for XHR Forms
*
* This script directly targets Form plugin's Dropzone initialization mechanisms
*/
(function() {
'use strict';
// Enable debugging logs
const DEBUG = false;
// Helper function for logging
function log(message, type = 'log') {
if (!DEBUG) return;
const prefix = '[Dropzone Direct Init]';
if (type === 'error') {
console.error(prefix, message);
} else if (type === 'warn') {
console.warn(prefix, message);
} else {
console.log(prefix, message);
}
}
// Flag to prevent multiple initializations
let isInitializing = false;
// Function to directly initialize Dropzone
function initializeDropzone(element) {
if (isInitializing) {
log('Initialization already in progress, skipping');
return false;
}
if (!element || element.classList.contains('dz-clickable')) {
return false;
}
log('Starting direct Dropzone initialization for element:', element);
isInitializing = true;
// First, let's try to find the FilesField constructor in the global scope
if (typeof FilesField === 'function') {
log('Found FilesField constructor, trying direct instantiation');
try {
new FilesField({
container: element,
options: {}
});
log('Successfully initialized Dropzone using FilesField constructor');
isInitializing = false;
return true;
} catch (e) {
log(`Error using FilesField constructor: ${e.message}`, 'error');
// Continue with other methods
}
}
// Second approach: Look for the Form plugin's initialization code in the page
const dropzoneInit = findFunctionOnWindow('addNode') ||
window.addNode ||
findFunctionOnWindow('initDropzone');
if (dropzoneInit) {
log('Found Form plugin initialization function, calling it directly');
try {
dropzoneInit(element);
log('Successfully called Form plugin initialization function');
isInitializing = false;
return true;
} catch (e) {
log(`Error calling Form plugin initialization function: ${e.message}`, 'error');
// Continue with other methods
}
}
// Third approach: Try to invoke Dropzone directly if it's globally available
if (typeof Dropzone === 'function') {
log('Found global Dropzone constructor, trying direct instantiation');
try {
// Extract settings from the element
const settingsAttr = element.getAttribute('data-grav-file-settings');
if (!settingsAttr) {
log('No settings found for element', 'warn');
isInitializing = false;
return false;
}
const settings = JSON.parse(settingsAttr);
const optionsAttr = element.getAttribute('data-dropzone-options');
const options = optionsAttr ? JSON.parse(optionsAttr) : {};
// Configure Dropzone options
const dropzoneOptions = {
url: element.getAttribute('data-file-url-add') || window.location.href,
maxFiles: settings.limit || null,
maxFilesize: settings.filesize || 10,
acceptedFiles: settings.accept ? settings.accept.join(',') : null
};
// Merge with any provided options
Object.assign(dropzoneOptions, options);
// Create new Dropzone instance
new Dropzone(element, dropzoneOptions);
log('Successfully initialized Dropzone using global constructor');
isInitializing = false;
return true;
} catch (e) {
log(`Error using global Dropzone constructor: ${e.message}`, 'error');
// Continue to final approach
}
}
// Final approach: Force reloading of Form plugin's JavaScript
log('Attempting to force reload Form plugin JavaScript');
// Look for Form plugin's JS files
const formVendorScript = document.querySelector('script[src*="form.vendor.js"]');
const formScript = document.querySelector('script[src*="form.min.js"]');
if (formVendorScript || formScript) {
log('Found Form plugin scripts, attempting to reload them');
// Create new script elements
if (formVendorScript) {
const newVendorScript = document.createElement('script');
newVendorScript.src = formVendorScript.src.split('?')[0] + '?t=' + new Date().getTime();
newVendorScript.async = true;
newVendorScript.onload = function() {
log('Reloaded Form vendor script');
// Trigger event after script loads
setTimeout(function() {
const event = new CustomEvent('mutation._grav', {
detail: { target: element }
});
document.body.dispatchEvent(event);
}, 100);
};
document.head.appendChild(newVendorScript);
}
if (formScript) {
const newFormScript = document.createElement('script');
newFormScript.src = formScript.src.split('?')[0] + '?t=' + new Date().getTime();
newFormScript.async = true;
newFormScript.onload = function() {
log('Reloaded Form script');
// Trigger event after script loads
setTimeout(function() {
const event = new CustomEvent('mutation._grav', {
detail: { target: element }
});
document.body.dispatchEvent(event);
}, 100);
};
document.head.appendChild(newFormScript);
}
}
// As a final resort, trigger the mutation event
log('Triggering mutation._grav event as final resort');
const event = new CustomEvent('mutation._grav', {
detail: { target: element }
});
document.body.dispatchEvent(event);
isInitializing = false;
return false;
}
// Helper function to find a function on the window object by name pattern
function findFunctionOnWindow(pattern) {
for (const key in window) {
if (typeof window[key] === 'function' && key.includes(pattern)) {
return window[key];
}
}
return null;
}
// Function to check all Dropzone elements
function checkAllDropzones() {
const dropzones = document.querySelectorAll('.dropzone.files-upload:not(.dz-clickable)');
if (dropzones.length === 0) {
log('No uninitialized Dropzone elements found');
return;
}
log(`Found ${dropzones.length} uninitialized Dropzone elements`);
// Try to initialize each one
dropzones.forEach(function(element) {
initializeDropzone(element);
});
}
// Hook into form submission to reinitialize after XHR updates
function setupFormSubmissionHook() {
// First check if the XHR submit function is available
if (window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
log('Found GravFormXHR.submit, attaching hook');
// Store the original function
const originalSubmit = window.GravFormXHR.submit;
// Override it with our version
window.GravFormXHR.submit = function(form) {
log(`XHR form submission detected for form: ${form?.id || 'unknown'}`);
// Call the original function
const result = originalSubmit.apply(this, arguments);
// Set up checks for after the submission completes
[500, 1000, 2000, 3000].forEach(function(delay) {
setTimeout(checkAllDropzones, delay);
});
return result;
};
log('Successfully hooked into GravFormXHR.submit');
}
// Also add a direct event listener for standard form submissions
document.addEventListener('submit', function(event) {
if (event.target.tagName === 'FORM') {
log(`Standard form submission detected for form: ${event.target.id || 'unknown'}`);
// Schedule checks after submission
[1000, 2000, 3000].forEach(function(delay) {
setTimeout(checkAllDropzones, delay);
});
}
});
log('Form submission hooks set up');
}
// Monitor for AJAX responses
function setupAjaxMonitoring() {
if (window.jQuery) {
log('Setting up jQuery AJAX response monitoring');
jQuery(document).ajaxComplete(function(event, xhr, settings) {
log('AJAX request completed, checking if form-related');
// Check if this looks like a form request
const url = settings.url || '';
if (url.includes('form') ||
url.includes('task=') ||
url.includes('file-upload') ||
url.includes('file-uploader')) {
log('Form-related AJAX request detected, will check for Dropzones');
// Schedule checks with delays
[300, 800, 1500].forEach(function(delay) {
setTimeout(checkAllDropzones, delay);
});
}
});
log('jQuery AJAX monitoring set up');
}
}
// Create global function for manual reinitialization
window.reinitializeDropzones = function() {
log('Manual reinitialization triggered');
checkAllDropzones();
return 'Reinitialization check triggered. See console for details.';
};
// Main initialization function
function initialize() {
log('Initializing Dropzone direct initialization system');
// Set up submission hook
setupFormSubmissionHook();
// Set up AJAX monitoring
setupAjaxMonitoring();
// Do an initial check for any uninitialized Dropzones
setTimeout(checkAllDropzones, 500);
log('Initialization complete. Use window.reinitializeDropzones() for manual reinitialization.');
}
// Start when the DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
// Delay to allow other scripts to load
setTimeout(initialize, 100);
});
} else {
// DOM already loaded, delay slightly
setTimeout(initialize, 100);
}
})();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,662 @@
/**
* 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');
})();

View File

@@ -0,0 +1,141 @@
/**
* FilePond Direct Fix - Emergency fix for XHR forms
*/
(function() {
// Directly attempt to initialize uninitialized FilePond elements
// without relying on any existing logic
console.log('FilePond Direct Fix loaded');
// Function to directly create FilePond instances
function initializeFilePondElements() {
console.log('Direct FilePond initialization attempt');
// Find uninitialized FilePond elements
const elements = document.querySelectorAll('.filepond-root:not(.filepond--hopper)');
if (elements.length === 0) {
return;
}
console.log(`Found ${elements.length} uninitialized FilePond elements`);
// Process each element
elements.forEach((element, index) => {
const input = element.querySelector('input[type="file"]:not(.filepond--browser)');
if (!input) {
console.log(`Element #${index + 1}: No suitable file input found`);
return;
}
console.log(`Element #${index + 1}: Found file input:`, input);
// Get settings
let settings = {};
try {
const settingsAttr = element.getAttribute('data-grav-file-settings');
if (settingsAttr) {
settings = JSON.parse(settingsAttr);
console.log('Parsed settings:', settings);
}
} catch (e) {
console.error('Failed to parse settings:', e);
}
// Get URLS
const uploadUrl = element.getAttribute('data-file-url-add');
const removeUrl = element.getAttribute('data-file-url-remove');
console.log('Upload URL:', uploadUrl);
console.log('Remove URL:', removeUrl);
try {
// Create FilePond instance directly
const pond = FilePond.create(input);
// Apply minimal configuration to make uploads work
if (pond) {
console.log(`Successfully created FilePond on element #${index + 1}`);
// Basic configuration to make it functional
pond.setOptions({
name: settings.paramName || input.name || 'files',
server: {
process: uploadUrl,
revert: removeUrl
},
// Transform options
imageTransformOutputMimeType: 'image/jpeg',
imageTransformOutputQuality: settings.resizeQuality || 90,
imageTransformOutputStripImageHead: true,
// Resize options
imageResizeTargetWidth: settings.resizeWidth || null,
imageResizeTargetHeight: settings.resizeHeight || null,
imageResizeMode: 'cover',
imageResizeUpscale: false
});
}
} catch (e) {
console.error(`Failed to create FilePond on element #${index + 1}:`, e);
}
});
}
// Monitor form submissions and DOM changes
function setupMonitoring() {
// Create MutationObserver to watch for DOM changes
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) {
console.log('DOM changes detected that might include FilePond elements');
// Delay to ensure DOM is fully updated
setTimeout(initializeFilePondElements, 50);
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('MutationObserver set up for FilePond elements');
}
}
// Set up the emergency fix
function init() {
// Set up monitoring
setupMonitoring();
// Expose global function for manual reinit
window.directFilePondInit = initializeFilePondElements;
// Initial check
setTimeout(initializeFilePondElements, 500);
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 0);
}
})();

View File

@@ -0,0 +1,9 @@
/*!
* FilePondPluginFileValidateSize 2.2.8
* Licensed under MIT, https://opensource.org/licenses/MIT/
* Please visit https://pqina.nl/filepond/ for details.
*/
/* eslint-disable */
!function(e,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(e=e||self).FilePondPluginFileValidateSize=i()}(this,function(){"use strict";var e=function(e){var i=e.addFilter,E=e.utils,l=E.Type,_=E.replaceInString,n=E.toNaturalFileSize;return i("ALLOW_HOPPER_ITEM",function(e,i){var E=i.query;if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return!0;var l=E("GET_MAX_FILE_SIZE");if(null!==l&&e.size>l)return!1;var _=E("GET_MIN_FILE_SIZE");return!(null!==_&&e.size<_)}),i("LOAD_FILE",function(e,i){var E=i.query;return new Promise(function(i,l){if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return i(e);var I=E("GET_FILE_VALIDATE_SIZE_FILTER");if(I&&!I(e))return i(e);var t=E("GET_MAX_FILE_SIZE");if(null!==t&&e.size>t)l({status:{main:E("GET_LABEL_MAX_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_FILE_SIZE"),{filesize:n(t,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});else{var L=E("GET_MIN_FILE_SIZE");if(null!==L&&e.size<L)l({status:{main:E("GET_LABEL_MIN_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MIN_FILE_SIZE"),{filesize:n(L,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});else{var a=E("GET_MAX_TOTAL_FILE_SIZE");if(null!==a)if(E("GET_ACTIVE_ITEMS").reduce(function(e,i){return e+i.fileSize},0)>a)return void l({status:{main:E("GET_LABEL_MAX_TOTAL_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_TOTAL_FILE_SIZE"),{filesize:n(a,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});i(e)}}})}),{options:{allowFileSizeValidation:[!0,l.BOOLEAN],maxFileSize:[null,l.INT],minFileSize:[null,l.INT],maxTotalFileSize:[null,l.INT],fileValidateSizeFilter:[null,l.FUNCTION],labelMinFileSizeExceeded:["File is too small",l.STRING],labelMinFileSize:["Minimum file size is {filesize}",l.STRING],labelMaxFileSizeExceeded:["File is too large",l.STRING],labelMaxFileSize:["Maximum file size is {filesize}",l.STRING],labelMaxTotalFileSizeExceeded:["Maximum total size exceeded",l.STRING],labelMaxTotalFileSize:["Maximum total file size is {filesize}",l.STRING]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e});

View File

@@ -0,0 +1,9 @@
/*!
* FilePondPluginFileValidateType 1.2.9
* Licensed under MIT, https://opensource.org/licenses/MIT/
* Please visit https://pqina.nl/filepond/ for details.
*/
/* eslint-disable */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).FilePondPluginFileValidateType=t()}(this,function(){"use strict";var e=function(e){var t=e.addFilter,n=e.utils,i=n.Type,T=n.isString,E=n.replaceInString,l=n.guesstimateMimeType,o=n.getExtensionFromFilename,r=n.getFilenameFromURL,u=function(e,t){return e.some(function(e){return/\*$/.test(e)?(n=e,(/^[^/]+/.exec(t)||[]).pop()===n.slice(0,-2)):e===t;var n})},a=function(e,t,n){if(0===t.length)return!0;var i=function(e){var t="";if(T(e)){var n=r(e),i=o(n);i&&(t=l(i))}else t=e.type;return t}(e);return n?new Promise(function(T,E){n(e,i).then(function(e){u(t,e)?T():E()}).catch(E)}):u(t,i)};return t("SET_ATTRIBUTE_TO_OPTION_MAP",function(e){return Object.assign(e,{accept:"acceptedFileTypes"})}),t("ALLOW_HOPPER_ITEM",function(e,t){var n=t.query;return!n("GET_ALLOW_FILE_TYPE_VALIDATION")||a(e,n("GET_ACCEPTED_FILE_TYPES"))}),t("LOAD_FILE",function(e,t){var n=t.query;return new Promise(function(t,i){if(n("GET_ALLOW_FILE_TYPE_VALIDATION")){var T=n("GET_ACCEPTED_FILE_TYPES"),l=n("GET_FILE_VALIDATE_TYPE_DETECT_TYPE"),o=a(e,T,l),r=function(){var e,t=T.map((e=n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES_MAP"),function(t){return null!==e[t]&&(e[t]||t)})).filter(function(e){return!1!==e}),l=t.filter(function(e,n){return t.indexOf(e)===n});i({status:{main:n("GET_LABEL_FILE_TYPE_NOT_ALLOWED"),sub:E(n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES"),{allTypes:l.join(", "),allButLastType:l.slice(0,-1).join(", "),lastType:l[l.length-1]})}})};if("boolean"==typeof o)return o?t(e):r();o.then(function(){t(e)}).catch(r)}else t(e)})}),{options:{allowFileTypeValidation:[!0,i.BOOLEAN],acceptedFileTypes:[[],i.ARRAY],labelFileTypeNotAllowed:["File is of invalid type",i.STRING],fileValidateTypeLabelExpectedTypes:["Expects {allButLastType} or {lastType}",i.STRING],fileValidateTypeLabelExpectedTypesMap:[{},i.OBJECT],fileValidateTypeDetectType:[null,i.FUNCTION]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e});

View File

@@ -0,0 +1,8 @@
/*!
* FilePondPluginImagePreview 4.6.12
* Licensed under MIT, https://opensource.org/licenses/MIT/
* Please visit https://pqina.nl/filepond/ for details.
*/
/* eslint-disable */
.filepond--image-preview-markup{position:absolute;left:0;top:0}.filepond--image-preview-wrapper{z-index:2}.filepond--image-preview-overlay{display:block;position:absolute;left:0;top:0;width:100%;min-height:5rem;max-height:7rem;margin:0;opacity:0;z-index:2;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.filepond--image-preview-overlay svg{width:100%;height:auto;color:inherit;max-height:inherit}.filepond--image-preview-overlay-idle{mix-blend-mode:multiply;color:rgba(40,40,40,.85)}.filepond--image-preview-overlay-success{mix-blend-mode:normal;color:#369763}.filepond--image-preview-overlay-failure{mix-blend-mode:normal;color:#c44e47}@supports (-webkit-marquee-repetition:infinite) and ((-o-object-fit:fill) or (object-fit:fill)){.filepond--image-preview-overlay-idle{mix-blend-mode:normal}}.filepond--image-preview-wrapper{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:absolute;left:0;top:0;right:0;height:100%;margin:0;border-radius:.45em;overflow:hidden;background:rgba(0,0,0,.01)}.filepond--image-preview{position:absolute;left:0;top:0;z-index:1;display:flex;align-items:center;height:100%;width:100%;pointer-events:none;background:#222;will-change:transform,opacity}.filepond--image-clip{position:relative;overflow:hidden;margin:0 auto}.filepond--image-clip[data-transparency-indicator=grid] canvas,.filepond--image-clip[data-transparency-indicator=grid] img{background-color:#fff;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg' fill='%23eee'%3E%3Cpath d='M0 0h50v50H0M50 50h50v50H50'/%3E%3C/svg%3E");background-size:1.25em 1.25em}.filepond--image-bitmap,.filepond--image-vector{position:absolute;left:0;top:0;will-change:transform}.filepond--root[data-style-panel-layout~=integrated] .filepond--image-preview-wrapper{border-radius:0}.filepond--root[data-style-panel-layout~=integrated] .filepond--image-preview{height:100%;display:flex;justify-content:center;align-items:center}.filepond--root[data-style-panel-layout~=circle] .filepond--image-preview-wrapper{border-radius:99999rem}.filepond--root[data-style-panel-layout~=circle] .filepond--image-preview-overlay{top:auto;bottom:0;-webkit-transform:scaleY(-1);transform:scaleY(-1)}.filepond--root[data-style-panel-layout~=circle] .filepond--file .filepond--file-action-button[data-align*=bottom]:not([data-align*=center]){margin-bottom:.325em}.filepond--root[data-style-panel-layout~=circle] .filepond--file [data-align*=left]{left:calc(50% - 3em)}.filepond--root[data-style-panel-layout~=circle] .filepond--file [data-align*=right]{right:calc(50% - 3em)}.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=left],.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=right]{margin-bottom:.5125em}.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=center]{margin-top:0;margin-bottom:.1875em;margin-left:.1875em}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
/*!
* FilePondPluginImageResize 2.0.10
* Licensed under MIT, https://opensource.org/licenses/MIT/
* Please visit https://pqina.nl/filepond/ for details.
*/
/* eslint-disable */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).FilePondPluginImageResize=t()}(this,function(){"use strict";var e=function(e){var t=e.addFilter,i=e.utils.Type;return t("DID_LOAD_ITEM",function(e,t){var i=t.query;return new Promise(function(t,n){var r=e.file;if(!function(e){return/^image/.test(e.type)}(r)||!i("GET_ALLOW_IMAGE_RESIZE"))return t(e);var u=i("GET_IMAGE_RESIZE_MODE"),o=i("GET_IMAGE_RESIZE_TARGET_WIDTH"),a=i("GET_IMAGE_RESIZE_TARGET_HEIGHT"),l=i("GET_IMAGE_RESIZE_UPSCALE");if(null===o&&null===a)return t(e);var d,f,E,s=null===o?a:o,c=null===a?s:a,I=URL.createObjectURL(r);d=I,f=function(i){if(URL.revokeObjectURL(I),!i)return t(e);var n=i.width,r=i.height,o=(e.getMetadata("exif")||{}).orientation||-1;if(o>=5&&o<=8){var a=[r,n];n=a[0],r=a[1]}if(n===s&&r===c)return t(e);if(!l)if("cover"===u){if(n<=s||r<=c)return t(e)}else if(n<=s&&r<=s)return t(e);e.setMetadata("resize",{mode:u,upscale:l,size:{width:s,height:c}}),t(e)},(E=new Image).onload=function(){var e=E.naturalWidth,t=E.naturalHeight;E=null,f({width:e,height:t})},E.onerror=function(){return f(null)},E.src=d})}),{options:{allowImageResize:[!0,i.BOOLEAN],imageResizeMode:["cover",i.STRING],imageResizeUpscale:[!0,i.BOOLEAN],imageResizeTargetWidth:[null,i.INT],imageResizeTargetHeight:[null,i.INT]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.form-group.has-errors{background:rgba(255,0,0,.05);border:1px solid rgba(255,0,0,.2);border-radius:3px;margin:0 -5px;padding:0 5px}.form-errors{color:#b52b27}.form-honeybear{display:none;position:absolute !important;height:1px;width:1px;overflow:hidden;clip-path:rect(0px, 1px, 1px, 0px)}.form-errors p{margin:0}.form-input-file input{display:none}.form-input-file .dz-default.dz-message{position:absolute;text-align:center;left:0;right:0;top:50%;transform:translateY(-50%);margin:0}.form-input-file.dropzone{position:relative;min-height:70px;border-radius:3px;margin-bottom:.85rem;border:2px dashed #ccc;color:#aaa;padding:.5rem}.form-input-file.dropzone .dz-preview{margin:.5rem}.form-input-file.dropzone .dz-preview:hover{z-index:2}.form-input-file.dropzone .dz-preview .dz-image img{margin:0}.form-input-file.dropzone .dz-preview .dz-remove{font-size:16px;position:absolute;top:3px;right:3px;display:inline-flex;height:20px;width:20px;background-color:red;justify-content:center;align-items:center;color:#fff;font-weight:bold;border-radius:50%;cursor:pointer;z-index:20}.form-input-file.dropzone .dz-preview .dz-remove:hover{background-color:darkred;text-decoration:none}.form-input-file.dropzone .dz-preview .dz-error-message{min-width:140px;width:auto}.form-input-file.dropzone .dz-preview .dz-image,.form-input-file.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:3px;z-index:1}.filepond--root.form-input{min-height:7rem;height:auto;overflow:hidden;border:0}.form-tabs .tabs-nav{display:flex;padding-top:1px;margin-bottom:-1px}.form-tabs .tabs-nav a{flex:1;transition:color .5s ease,background .5s ease;cursor:pointer;text-align:center;padding:10px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid #ccc;border-radius:5px 5px 0 0}.form-tabs .tabs-nav a.active{border:1px solid #ccc;border-bottom:1px solid rgba(0,0,0,0);margin:0 -1px}.form-tabs .tabs-nav a.active span{color:#000}.form-tabs .tabs-nav span{display:inline-block;line-height:1.1}.form-tabs.subtle .tabs-nav{margin-right:0 !important}.form-tabs .tabs-content .tab__content{display:none;padding-top:2rem}.form-tabs .tabs-content .tab__content.active{display:block}.checkboxes{display:inline-block}.checkboxes label{display:inline;cursor:pointer;position:relative;padding:0 0 0 20px;margin-right:15px}.checkboxes label:before{content:"";display:inline-block;width:20px;height:20px;left:0;margin-top:0;margin-right:10px;position:absolute;border-radius:3px;border:1px solid #e6e6e6}.checkboxes input[type=checkbox]{display:none}.checkboxes input[type=checkbox]:checked+label:before{content:"✓";font-size:20px;line-height:1;text-align:center}.checkboxes.toggleable label{margin-right:0}.form-field-toggleable .checkboxes.toggleable{margin-right:5px;vertical-align:middle}.form-field-toggleable .checkboxes+label{display:inline-block}.switch-toggle{display:inline-flex;overflow:hidden;border-radius:3px;line-height:35px;border:1px solid #ccc}.switch-toggle input[type=radio]{position:absolute;visibility:hidden;display:none}.switch-toggle label{display:inline-block;cursor:pointer;padding:0 15px;margin:0;white-space:nowrap;color:inherit;transition:background-color .5s ease}.switch-toggle input.highlight:checked+label{background:#333;color:#fff}.switch-toggle input:checked+label{color:#fff;background:#999}.signature-pad{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-size:10px;width:100%;height:100%;max-width:700px;max-height:460px;border:1px solid #f0f0f0;background-color:#fff;padding:16px}.signature-pad--body{position:relative;-webkit-box-flex:1;-ms-flex:1;flex:1;border:1px solid #f6f6f6;min-height:100px}.signature-pad--body canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px rgba(0,0,0,.02) inset}.signature-pad--footer{color:#c3c3c3;text-align:center;font-size:1.2em}.signature-pad--actions{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-top:8px}[data-grav-field=array] .form-row{display:flex;align-items:center;margin-bottom:.5rem}[data-grav-field=array] .form-row>input,[data-grav-field=array] .form-row>textarea{margin:0 .5rem;display:inline-block}.form-data.basic-captcha .form-input-wrapper{border:1px solid #ccc;border-radius:5px;display:flex;overflow:hidden}.form-data.basic-captcha .form-input-prepend{display:flex;color:#333;background-color:#ccc;flex-shrink:0}.form-data.basic-captcha .form-input-prepend img{margin:0}.form-data.basic-captcha .form-input-prepend button>svg{margin:0 8px;width:18px;height:18px}.form-data.basic-captcha input.form-input{border:0}/*# sourceMappingURL=form-styles.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../scss/form-styles.scss"],"names":[],"mappings":"CAGA,uBACI,6BACA,kCACA,kBACA,cACA,cAGJ,aACI,cAGJ,gBACI,aACA,6BACA,WACA,UACA,gBACA,mCAGJ,eACI,SAKA,uBACI,aAGJ,wCACI,kBACA,kBACA,OACA,QACA,QACA,2BACA,SAGJ,0BACI,kBACA,gBACA,kBACA,qBACA,uBACA,WACA,cAEA,sCACI,aAEA,4CACI,UAGJ,oDACE,SAGF,iDACE,eACA,kBACA,QACA,UACA,oBACA,YACA,WACA,qBACA,uBACA,mBACA,WACA,iBACA,kBACA,eACA,WACA,uDACI,yBACA,qBAIN,wDACI,gBACA,WAGJ,gHAEI,kBACA,UAOhB,2BACE,gBACA,YACA,gBACA,SAME,qBACI,aACA,gBAEA,mBAEA,uBACI,OACA,8CACA,eACA,kBACA,aACA,aACA,mBACA,uBACA,6BACA,0BAEA,8BACI,sBACA,sCACA,cAEA,mCACI,MAtIA,KA2IZ,0BACI,qBACA,gBAKR,4BACI,0BAKA,uCACI,aACA,iBAEA,8CACI,cAOhB,YACI,qBAEA,kBACI,eACA,eACA,kBACA,mBACA,kBAGJ,yBACI,WACA,qBACA,WACA,YACA,OACA,aACA,kBACA,kBACA,kBAEA,yBAGJ,iCACI,aAEJ,sDACI,YACA,eACA,cACA,kBAGJ,6BACI,eAMJ,8CACI,iBACA,sBAEJ,yCACI,qBAKR,eACI,oBACA,gBACA,kBACA,iBACA,sBAEA,iCACI,kBACA,kBACA,aAGJ,qBACI,qBACA,eACA,eACA,SACA,mBACA,cACA,qCAGJ,6CACI,gBACA,WAGJ,mCACI,WACA,gBAOR,eACI,kBACA,oBACA,oBACA,aACA,4BACA,6BACA,0BACA,sBACA,eACA,WACA,YACA,gBACA,iBACA,yBACA,sBACA,aAGJ,qBACI,kBACA,mBACA,WACA,OACA,yBACA,iBAGJ,4BACI,kBACA,OACA,MACA,WACA,YACA,kBACA,yCAGJ,uBACI,cACA,kBACA,gBAGJ,wBACI,oBACA,oBACA,aACA,yBACA,sBACA,8BACA,eAGJ,kCACI,aACA,mBACA,oBAGJ,mFAGI,eACA,qBAIA,6CACI,sBACA,kBACA,aACA,gBAEJ,6CACI,aACA,WACA,sBACA,cACA,iDACI,SAEJ,wDACI,aACA,WACA,YAGR,0CACI","file":"form-styles.css"}

View File

@@ -0,0 +1 @@
.form-group.has-errors{margin:0 -5px;padding:0 5px;border:1px solid rgba(255,0,0,.2);border-radius:3px;background:rgba(255,0,0,.05)}.form-errors{color:#b52b27}.form-honeybear{position:absolute!important;visibility:hidden;overflow:hidden;clip:rect(1px,1px,1px,1px);width:1px;height:1px}.form-errors p{margin:0}.form-input-file input{display:none}.form-input-file .dz-default.dz-message{position:absolute;top:50%;right:0;left:0;margin:0;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center}.form-input-file.dropzone{position:relative;min-height:70px;margin-bottom:.85rem;padding:.5rem;color:#aaa;border:2px dashed #ccc;border-radius:3px}.form-input-file.dropzone .dz-preview{margin:.5rem}.form-input-file.dropzone .dz-preview:hover{z-index:2}.form-input-file.dropzone .dz-preview .dz-error-message{width:auto;min-width:140px}.form-input-file.dropzone .dz-preview .dz-image,.form-input-file.dropzone .dz-preview.dz-file-preview .dz-image{z-index:1;border-radius:3px}.form-tabs .tabs-nav{display:flex;margin-bottom:-1px;padding-top:1px}.form-tabs .tabs-nav a{display:flex;padding:10px;cursor:pointer;transition:color .5s ease,background .5s ease;text-align:center;border-bottom:1px solid #eee;border-radius:5px 5px 0 0;flex:1;align-items:center;justify-content:center}.form-tabs .tabs-nav a.active{margin:0 -1px;border:1px solid #eee;border-bottom:1px solid transparent}.form-tabs .tabs-nav a.active span{color:#000}.form-tabs .tabs-nav span{line-height:1.1;display:inline-block}.form-tabs.subtle .tabs-nav{margin-right:0!important}.form-tabs .tabs-content .tab__content{display:none;padding-top:2rem}.form-tabs .tabs-content .tab__content.active{display:block}.checkboxes{display:inline-block}.checkboxes label{position:relative;display:inline;margin-right:15px;padding:0 0 0 20px;cursor:pointer}.checkboxes label:before{position:absolute;left:0;display:inline-block;width:20px;height:20px;margin-top:0;margin-right:10px;content:'';border:1px solid #e6e6e6;border-radius:3px}.checkboxes input[type=checkbox]{display:none}.checkboxes input[type=checkbox]:checked+label:before{font-size:20px;line-height:1;content:'\2713';text-align:center}.checkboxes.toggleable label{margin-right:0}.form-field-toggleable .checkboxes.toggleable{margin-right:5px;vertical-align:middle}.form-field-toggleable .checkboxes+label{display:inline-block}.switch-toggle{line-height:35px;display:inline-flex;overflow:hidden;border:1px solid #eee;border-radius:3px}.switch-toggle input[type=radio]{position:absolute;display:none;visibility:hidden}.switch-toggle label{display:inline-block;margin:0;padding:0 15px;cursor:pointer;transition:background-color .5s ease;white-space:nowrap;color:inherit}.switch-toggle input.highlight:checked+label{color:#fff;background:#333}.switch-toggle input:checked+label{color:#fff;background:#999}.signature-pad{font-size:10px;position:relative;display:flex;flex-direction:column;width:100%;max-width:700px;height:100%;max-height:460px;padding:16px;border:1px solid #f0f0f0;background-color:#fff}.signature-pad--body{position:relative;min-height:100px;border:1px solid #f6f6f6;flex:1}.signature-pad--body canvas{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px rgba(0,0,0,.02) inset}.signature-pad--footer{font-size:1.2em;text-align:center;color:#c3c3c3}.signature-pad--actions{display:flex;margin-top:8px;justify-content:space-between}[data-grav-field=array] .form-row{display:flex;margin-bottom:.5rem;align-items:center}[data-grav-field=array] .form-row>input,[data-grav-field=array] .form-row>textarea{display:inline-block;margin:0 .5rem}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
if (typeof Object.assign !== 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, 'assign', {
value: function assign(target, varArgs) { // .length of function is 2
'use strict';
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}

View File

@@ -0,0 +1,617 @@
/*!
* Signature Pad v2.3.2
* https://github.com/szimek/signature_pad
*
* Copyright 2017 Szymon Nowak
* Released under the MIT license
*
* The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from:
* http://corner.squareup.com/2012/07/smoother-signatures.html
*
* Implementation of interpolation using cubic Bézier curves is taken from:
* http://benknowscode.wordpress.com/2012/09/14/path-interpolation-using-cubic-bezier-and-control-point-estimation-in-javascript
*
* Algorithm for approximated length of a Bézier curve is taken from:
* http://www.lemoda.net/maths/bezier-length/index.html
*
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.SignaturePad = factory());
}(this, (function () { 'use strict';
function Point(x, y, time) {
this.x = x;
this.y = y;
this.time = time || new Date().getTime();
}
Point.prototype.velocityFrom = function (start) {
return this.time !== start.time ? this.distanceTo(start) / (this.time - start.time) : 1;
};
Point.prototype.distanceTo = function (start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
};
Point.prototype.equals = function (other) {
return this.x === other.x && this.y === other.y && this.time === other.time;
};
function Bezier(startPoint, control1, control2, endPoint) {
this.startPoint = startPoint;
this.control1 = control1;
this.control2 = control2;
this.endPoint = endPoint;
}
// Returns approximated length.
Bezier.prototype.length = function () {
var steps = 10;
var length = 0;
var px = void 0;
var py = void 0;
for (var i = 0; i <= steps; i += 1) {
var t = i / steps;
var cx = this._point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
var cy = this._point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
var xdiff = cx - px;
var ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
};
/* eslint-disable no-multi-spaces, space-in-parens */
Bezier.prototype._point = function (t, start, c1, c2, end) {
return start * (1.0 - t) * (1.0 - t) * (1.0 - t) + 3.0 * c1 * (1.0 - t) * (1.0 - t) * t + 3.0 * c2 * (1.0 - t) * t * t + end * t * t * t;
};
/* eslint-disable */
// http://stackoverflow.com/a/27078401/815507
function throttle(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function later() {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function () {
var now = Date.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
}
function SignaturePad(canvas, options) {
var self = this;
var opts = options || {};
this.velocityFilterWeight = opts.velocityFilterWeight || 0.7;
this.minWidth = opts.minWidth || 0.5;
this.maxWidth = opts.maxWidth || 2.5;
this.throttle = 'throttle' in opts ? opts.throttle : 16; // in miliseconds
this.minDistance = 'minDistance' in opts ? opts.minDistance : 5;
if (this.throttle) {
this._strokeMoveUpdate = throttle(SignaturePad.prototype._strokeUpdate, this.throttle);
} else {
this._strokeMoveUpdate = SignaturePad.prototype._strokeUpdate;
}
this.dotSize = opts.dotSize || function () {
return (this.minWidth + this.maxWidth) / 2;
};
this.penColor = opts.penColor || 'black';
this.backgroundColor = opts.backgroundColor || 'rgba(0,0,0,0)';
this.onBegin = opts.onBegin;
this.onEnd = opts.onEnd;
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
this.clear();
// We need add these inline so they are available to unbind while still having
// access to 'self' we could use _.bind but it's not worth adding a dependency.
this._handleMouseDown = function (event) {
if (event.which === 1) {
self._mouseButtonDown = true;
self._strokeBegin(event);
}
};
this._handleMouseMove = function (event) {
if (self._mouseButtonDown) {
self._strokeMoveUpdate(event);
}
};
this._handleMouseUp = function (event) {
if (event.which === 1 && self._mouseButtonDown) {
self._mouseButtonDown = false;
self._strokeEnd(event);
}
};
this._handleTouchStart = function (event) {
// Prevent scrolling.
event.preventDefault();
if (event.targetTouches.length === 1) {
var touch = event.changedTouches[0];
self._strokeBegin(touch);
}
};
this._handleTouchMove = function (event) {
// Prevent scrolling.
event.preventDefault();
var touch = event.targetTouches[0];
self._strokeMoveUpdate(touch);
};
this._handleTouchEnd = function (event) {
var wasCanvasTouched = event.target === self._canvas;
if (wasCanvasTouched) {
event.preventDefault();
self._strokeEnd(event);
}
};
// Enable mouse and touch event handlers
this.on();
}
// Public methods
SignaturePad.prototype.clear = function () {
var ctx = this._ctx;
var canvas = this._canvas;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset();
this._isEmpty = true;
};
SignaturePad.prototype.fromDataURL = function (dataUrl) {
var _this = this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var image = new Image();
var ratio = options.ratio || window.devicePixelRatio || 1;
var width = options.width || this._canvas.width / ratio;
var height = options.height || this._canvas.height / ratio;
this._reset();
image.src = dataUrl;
image.onload = function () {
_this._ctx.drawImage(image, 0, 0, width, height);
};
this._isEmpty = false;
};
SignaturePad.prototype.toDataURL = function (type) {
var _canvas;
switch (type) {
case 'image/svg+xml':
return this._toSVG();
default:
for (var _len = arguments.length, options = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
options[_key - 1] = arguments[_key];
}
return (_canvas = this._canvas).toDataURL.apply(_canvas, [type].concat(options));
}
};
SignaturePad.prototype.on = function () {
this._handleMouseEvents();
this._handleTouchEvents();
};
SignaturePad.prototype.off = function () {
// Pass touch events to canvas element on mobile IE11 and Edge.
this._canvas.style.msTouchAction = 'auto';
this._canvas.style.touchAction = 'auto';
this._canvas.removeEventListener('mousedown', this._handleMouseDown);
this._canvas.removeEventListener('mousemove', this._handleMouseMove);
document.removeEventListener('mouseup', this._handleMouseUp);
this._canvas.removeEventListener('touchstart', this._handleTouchStart);
this._canvas.removeEventListener('touchmove', this._handleTouchMove);
this._canvas.removeEventListener('touchend', this._handleTouchEnd);
};
SignaturePad.prototype.isEmpty = function () {
return this._isEmpty;
};
// Private methods
SignaturePad.prototype._strokeBegin = function (event) {
this._data.push([]);
this._reset();
this._strokeUpdate(event);
if (typeof this.onBegin === 'function') {
this.onBegin(event);
}
};
SignaturePad.prototype._strokeUpdate = function (event) {
var x = event.clientX;
var y = event.clientY;
var point = this._createPoint(x, y);
var lastPointGroup = this._data[this._data.length - 1];
var lastPoint = lastPointGroup && lastPointGroup[lastPointGroup.length - 1];
var isLastPointTooClose = lastPoint && point.distanceTo(lastPoint) < this.minDistance;
// Skip this point if it's too close to the previous one
if (!(lastPoint && isLastPointTooClose)) {
var _addPoint = this._addPoint(point),
curve = _addPoint.curve,
widths = _addPoint.widths;
if (curve && widths) {
this._drawCurve(curve, widths.start, widths.end);
}
this._data[this._data.length - 1].push({
x: point.x,
y: point.y,
time: point.time,
color: this.penColor
});
}
};
SignaturePad.prototype._strokeEnd = function (event) {
var canDrawCurve = this.points.length > 2;
var point = this.points[0]; // Point instance
if (!canDrawCurve && point) {
this._drawDot(point);
}
if (point) {
var lastPointGroup = this._data[this._data.length - 1];
var lastPoint = lastPointGroup[lastPointGroup.length - 1]; // plain object
// When drawing a dot, there's only one point in a group, so without this check
// such group would end up with exactly the same 2 points.
if (!point.equals(lastPoint)) {
lastPointGroup.push({
x: point.x,
y: point.y,
time: point.time,
color: this.penColor
});
}
}
if (typeof this.onEnd === 'function') {
this.onEnd(event);
}
};
SignaturePad.prototype._handleMouseEvents = function () {
this._mouseButtonDown = false;
this._canvas.addEventListener('mousedown', this._handleMouseDown);
this._canvas.addEventListener('mousemove', this._handleMouseMove);
document.addEventListener('mouseup', this._handleMouseUp);
};
SignaturePad.prototype._handleTouchEvents = function () {
// Pass touch events to canvas element on mobile IE11 and Edge.
this._canvas.style.msTouchAction = 'none';
this._canvas.style.touchAction = 'none';
this._canvas.addEventListener('touchstart', this._handleTouchStart);
this._canvas.addEventListener('touchmove', this._handleTouchMove);
this._canvas.addEventListener('touchend', this._handleTouchEnd);
};
SignaturePad.prototype._reset = function () {
this.points = [];
this._lastVelocity = 0;
this._lastWidth = (this.minWidth + this.maxWidth) / 2;
this._ctx.fillStyle = this.penColor;
};
SignaturePad.prototype._createPoint = function (x, y, time) {
var rect = this._canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, time || new Date().getTime());
};
SignaturePad.prototype._addPoint = function (point) {
var points = this.points;
var tmp = void 0;
points.push(point);
if (points.length > 2) {
// To reduce the initial lag make it work with 3 points
// by copying the first point to the beginning.
if (points.length === 3) points.unshift(points[0]);
tmp = this._calculateCurveControlPoints(points[0], points[1], points[2]);
var c2 = tmp.c2;
tmp = this._calculateCurveControlPoints(points[1], points[2], points[3]);
var c3 = tmp.c1;
var curve = new Bezier(points[1], c2, c3, points[2]);
var widths = this._calculateCurveWidths(curve);
// Remove the first element from the list,
// so that we always have no more than 4 points in points array.
points.shift();
return { curve: curve, widths: widths };
}
return {};
};
SignaturePad.prototype._calculateCurveControlPoints = function (s1, s2, s3) {
var dx1 = s1.x - s2.x;
var dy1 = s1.y - s2.y;
var dx2 = s2.x - s3.x;
var dy2 = s2.y - s3.y;
var m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
var m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
var l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
var l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
var dxm = m1.x - m2.x;
var dym = m1.y - m2.y;
var k = l2 / (l1 + l2);
var cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
var tx = s2.x - cm.x;
var ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty)
};
};
SignaturePad.prototype._calculateCurveWidths = function (curve) {
var startPoint = curve.startPoint;
var endPoint = curve.endPoint;
var widths = { start: null, end: null };
var velocity = this.velocityFilterWeight * endPoint.velocityFrom(startPoint) + (1 - this.velocityFilterWeight) * this._lastVelocity;
var newWidth = this._strokeWidth(velocity);
widths.start = this._lastWidth;
widths.end = newWidth;
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
};
SignaturePad.prototype._strokeWidth = function (velocity) {
return Math.max(this.maxWidth / (velocity + 1), this.minWidth);
};
SignaturePad.prototype._drawPoint = function (x, y, size) {
var ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, size, 0, 2 * Math.PI, false);
this._isEmpty = false;
};
SignaturePad.prototype._drawCurve = function (curve, startWidth, endWidth) {
var ctx = this._ctx;
var widthDelta = endWidth - startWidth;
var drawSteps = Math.floor(curve.length());
ctx.beginPath();
for (var i = 0; i < drawSteps; i += 1) {
// Calculate the Bezier (x, y) coordinate for this step.
var t = i / drawSteps;
var tt = t * t;
var ttt = tt * t;
var u = 1 - t;
var uu = u * u;
var uuu = uu * u;
var x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
var y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
var width = startWidth + ttt * widthDelta;
this._drawPoint(x, y, width);
}
ctx.closePath();
ctx.fill();
};
SignaturePad.prototype._drawDot = function (point) {
var ctx = this._ctx;
var width = typeof this.dotSize === 'function' ? this.dotSize() : this.dotSize;
ctx.beginPath();
this._drawPoint(point.x, point.y, width);
ctx.closePath();
ctx.fill();
};
SignaturePad.prototype._fromData = function (pointGroups, drawCurve, drawDot) {
for (var i = 0; i < pointGroups.length; i += 1) {
var group = pointGroups[i];
if (group.length > 1) {
for (var j = 0; j < group.length; j += 1) {
var rawPoint = group[j];
var point = new Point(rawPoint.x, rawPoint.y, rawPoint.time);
var color = rawPoint.color;
if (j === 0) {
// First point in a group. Nothing to draw yet.
// All points in the group have the same color, so it's enough to set
// penColor just at the beginning.
this.penColor = color;
this._reset();
this._addPoint(point);
} else if (j !== group.length - 1) {
// Middle point in a group.
var _addPoint2 = this._addPoint(point),
curve = _addPoint2.curve,
widths = _addPoint2.widths;
if (curve && widths) {
drawCurve(curve, widths, color);
}
} else {
// Last point in a group. Do nothing.
}
}
} else {
this._reset();
var _rawPoint = group[0];
drawDot(_rawPoint);
}
}
};
SignaturePad.prototype._toSVG = function () {
var _this2 = this;
var pointGroups = this._data;
var canvas = this._canvas;
var ratio = Math.max(window.devicePixelRatio || 1, 1);
var minX = 0;
var minY = 0;
var maxX = canvas.width / ratio;
var maxY = canvas.height / ratio;
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttributeNS(null, 'width', canvas.width);
svg.setAttributeNS(null, 'height', canvas.height);
this._fromData(pointGroups, function (curve, widths, color) {
var path = document.createElement('path');
// Need to check curve for NaN values, these pop up when drawing
// lines on the canvas that are not continuous. E.g. Sharp corners
// or stopping mid-stroke and than continuing without lifting mouse.
if (!isNaN(curve.control1.x) && !isNaN(curve.control1.y) && !isNaN(curve.control2.x) && !isNaN(curve.control2.y)) {
var attr = 'M ' + curve.startPoint.x.toFixed(3) + ',' + curve.startPoint.y.toFixed(3) + ' ' + ('C ' + curve.control1.x.toFixed(3) + ',' + curve.control1.y.toFixed(3) + ' ') + (curve.control2.x.toFixed(3) + ',' + curve.control2.y.toFixed(3) + ' ') + (curve.endPoint.x.toFixed(3) + ',' + curve.endPoint.y.toFixed(3));
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (widths.end * 2.25).toFixed(3));
path.setAttribute('stroke', color);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, function (rawPoint) {
var circle = document.createElement('circle');
var dotSize = typeof _this2.dotSize === 'function' ? _this2.dotSize() : _this2.dotSize;
circle.setAttribute('r', dotSize);
circle.setAttribute('cx', rawPoint.x);
circle.setAttribute('cy', rawPoint.y);
circle.setAttribute('fill', rawPoint.color);
svg.appendChild(circle);
});
var prefix = 'data:image/svg+xml;base64,';
var header = '<svg' + ' xmlns="http://www.w3.org/2000/svg"' + ' xmlns:xlink="http://www.w3.org/1999/xlink"' + (' viewBox="' + minX + ' ' + minY + ' ' + maxX + ' ' + maxY + '"') + (' width="' + maxX + '"') + (' height="' + maxY + '"') + '>';
var body = svg.innerHTML;
// IE hack for missing innerHTML property on SVGElement
if (body === undefined) {
var dummy = document.createElement('dummy');
var nodes = svg.childNodes;
dummy.innerHTML = '';
for (var i = 0; i < nodes.length; i += 1) {
dummy.appendChild(nodes[i].cloneNode(true));
}
body = dummy.innerHTML;
}
var footer = '</svg>';
var data = header + body + footer;
return prefix + btoa(data);
};
SignaturePad.prototype.fromData = function (pointGroups) {
var _this3 = this;
this.clear();
this._fromData(pointGroups, function (curve, widths) {
return _this3._drawCurve(curve, widths.start, widths.end);
}, function (rawPoint) {
return _this3._drawDot(rawPoint);
});
this._data = pointGroups;
};
SignaturePad.prototype.toData = function () {
return this._data;
};
return SignaturePad;
})));

View File

@@ -0,0 +1,461 @@
/**
* Grav Form XHR Submitter
*
* A modular system for handling form submissions via XMLHttpRequest (AJAX).
* Features include content replacement, captcha handling, and error management.
*/
(function() {
'use strict';
// Main namespace
window.GravFormXHR = {};
/**
* Core Module - Contains configuration and utility functions
*/
const Core = {
config: {
debug: false,
enableLoadingIndicator: false
},
/**
* Configure global settings
* @param {Object} options - Configuration options
*/
configure: function(options) {
Object.assign(this.config, options);
},
/**
* Logger utility
* @param {string} message - Message to log
* @param {string} level - Log level ('log', 'warn', 'error')
*/
log: function(message, level = 'log') {
if (!this.config.debug) return;
const validLevels = ['log', 'warn', 'error'];
const finalLevel = validLevels.includes(level) ? level : 'log';
console[finalLevel](`[GravFormXHR] ${message}`);
},
/**
* Display an error message within a target element
* @param {HTMLElement} target - The element to display the error in
* @param {string} message - The error message
*/
displayError: function(target, message) {
const errorMsgContainer = target.querySelector('.form-messages') || target;
const errorMsg = document.createElement('div');
errorMsg.className = 'form-message error';
errorMsg.textContent = message;
errorMsgContainer.insertBefore(errorMsg, errorMsgContainer.firstChild);
}
};
/**
* DOM Module - Handles DOM manipulation and form tracking
*/
const DOM = {
/**
* Find a form wrapper by formId
* @param {string} formId - ID of the form
* @returns {HTMLElement|null} - The wrapper element or null
*/
getFormWrapper: function(formId) {
const wrapperId = formId + '-wrapper';
return document.getElementById(wrapperId);
},
/**
* Add or remove loading indicators
* @param {HTMLElement} form - The form element
* @param {HTMLElement} wrapper - The wrapper element
* @param {boolean} isLoading - Whether to add or remove loading classes
*/
updateLoadingState: function(form, wrapper, isLoading) {
if (!Core.config.enableLoadingIndicator) return;
if (isLoading) {
wrapper.classList.add('loading');
form.classList.add('submitting');
} else {
wrapper.classList.remove('loading');
form.classList.remove('submitting');
}
},
/**
* Update form content with server response
* @param {string} responseText - Server response HTML
* @param {string} wrapperId - ID of the wrapper to update
* @param {string} formId - ID of the original form
*/
updateFormContent: function(responseText, wrapperId, formId) {
const wrapperElement = document.getElementById(wrapperId);
if (!wrapperElement) {
console.error(`Cannot update content: Wrapper #${wrapperId} not found`);
return;
}
Core.log(`Updating content for wrapper: ${wrapperId}`);
// Parse response
const tempDiv = document.createElement('div');
try {
tempDiv.innerHTML = responseText;
} catch (e) {
console.error(`Error parsing response HTML for wrapper: ${wrapperId}`, e);
Core.displayError(wrapperElement, 'An error occurred processing the server response.');
return;
}
try {
this._updateWrapperContent(tempDiv, wrapperElement, wrapperId, formId);
this._reinitializeUpdatedForm(wrapperElement, formId);
} catch (e) {
console.error(`Error during content update for wrapper ${wrapperId}:`, e);
Core.displayError(wrapperElement, 'An error occurred updating the form content.');
}
},
/**
* Update wrapper content based on response parsing strategy
* @private
*/
_updateWrapperContent: function(tempDiv, wrapperElement, wrapperId, formId) {
// Strategy 1: Look for matching wrapper ID in response
const newWrapperElement = tempDiv.querySelector('#' + wrapperId);
if (newWrapperElement) {
wrapperElement.innerHTML = newWrapperElement.innerHTML;
Core.log(`Update using newWrapperElement.innerHTML SUCCESSFUL for wrapper: ${wrapperId}`);
return;
}
// Strategy 2: Look for matching form ID in response
const hasMatchingForm = tempDiv.querySelector('#' + formId);
if (hasMatchingForm) {
Core.log(`Wrapper element #${wrapperId} not found in XHR response, but found matching form. Using entire response.`);
wrapperElement.innerHTML = tempDiv.innerHTML;
return;
}
// Strategy 3: Look for toast messages
const hasToastMessages = tempDiv.querySelector('.toast');
if (hasToastMessages) {
Core.log('Found toast messages in response. Updating wrapper with the response.');
wrapperElement.innerHTML = tempDiv.innerHTML;
return;
}
// Fallback: Use entire response with warning
Core.log('No matching content found in response. Response may not be valid for this wrapper.', 'warn');
wrapperElement.innerHTML = tempDiv.innerHTML;
},
/**
* Reinitialize updated form and its components
* @private
*/
_reinitializeUpdatedForm: function(wrapperElement, formId) {
const updatedForm = wrapperElement.querySelector('#' + formId);
if (updatedForm) {
Core.log(`Re-running initialization for form ${formId} after update`);
// First reinitialize any captchas
CaptchaManager.reinitializeAll(updatedForm);
// Trigger mutation._grav event for Dropzone and other field reinitializations
setTimeout(() => {
Core.log('Triggering mutation._grav event for field reinitialization');
// Trigger using jQuery if available (preferred method for compatibility)
if (typeof jQuery !== 'undefined') {
jQuery('body').trigger('mutation._grav', [wrapperElement]);
} else {
// Fallback: dispatch native custom event
const event = new CustomEvent('mutation._grav', {
detail: { target: wrapperElement },
bubbles: true
});
document.body.dispatchEvent(event);
}
}, 0);
// Then re-attach the XHR listener
setTimeout(() => {
FormHandler.setupListener(formId);
}, 10);
} else {
// Check if this was a successful submission with just a message
const hasSuccessMessage = wrapperElement.querySelector('.toast-success, .form-success');
if (hasSuccessMessage) {
Core.log('No form found after update, but success message detected. This appears to be a successful submission.');
} else {
console.warn(`Could not find form #${formId} inside the updated wrapper after update. Cannot re-attach listener/initializers.`);
}
}
}
};
/**
* XHR Module - Handles XMLHttpRequest operations
*/
const XHRManager = {
/**
* Send form data via XHR
* @param {HTMLFormElement} form - The form to submit
*/
sendFormData: function(form) {
const formId = form.id;
const wrapperId = formId + '-wrapper';
const wrapperElement = DOM.getFormWrapper(formId);
if (!wrapperElement) {
console.error(`XHR submission: Target wrapper element #${wrapperId} not found on the page! Cannot proceed.`);
form.innerHTML = '<p class="form-message error">Error: Form wrapper missing. Cannot update content.</p>';
return;
}
Core.log(`Initiating XHR submission for form: ${formId}, targeting wrapper: ${wrapperId}`);
DOM.updateLoadingState(form, wrapperElement, true);
const xhr = new XMLHttpRequest();
xhr.open(form.getAttribute('method') || 'POST', form.getAttribute('action') || window.location.href);
// Set Headers
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('X-Grav-Form-XHR', 'true');
// Success handler
xhr.onload = () => {
Core.log(`XHR request completed for form: ${formId}, Status: ${xhr.status}`);
DOM.updateLoadingState(form, wrapperElement, false);
if (xhr.status >= 200 && xhr.status < 300) {
DOM.updateFormContent(xhr.responseText, wrapperId, formId);
} else {
Core.log(`Form submission failed for form: ${formId}, HTTP Status: ${xhr.status} ${xhr.statusText}`, 'error');
Core.displayError(wrapperElement, `An error occurred during submission (Status: ${xhr.status}). Please check the form and try again.`);
}
};
// Network error handler
xhr.onerror = () => {
Core.log(`Form submission failed due to network error for form: ${formId}`, 'error');
DOM.updateLoadingState(form, wrapperElement, false);
Core.displayError(wrapperElement, 'A network error occurred. Please check your connection and try again.');
};
// Prepare and send data
try {
const formData = new FormData(form);
const urlEncodedData = new URLSearchParams(formData).toString();
Core.log(`Sending XHR request for form: ${formId} with custom header X-Grav-Form-XHR`);
xhr.send(urlEncodedData);
} catch (e) {
Core.log(`Error preparing or sending XHR request for form: ${formId}: ${e.message}`, 'error');
DOM.updateLoadingState(form, wrapperElement, false);
Core.displayError(wrapperElement, 'An unexpected error occurred before sending the form.');
}
}
};
/**
* CaptchaManager - Handles captcha registration and initialization
*/
const CaptchaManager = {
providers: {},
/**
* Register a captcha provider
* @param {string} name - Provider name
* @param {object} provider - Provider object with init and reset methods
*/
register: function(name, provider) {
this.providers[name] = provider;
Core.log(`Registered captcha provider: ${name}`);
},
/**
* Get a provider by name
* @param {string} name - Provider name
* @returns {object|null} Provider object or null if not found
*/
getProvider: function(name) {
return this.providers[name] || null;
},
/**
* Get all registered providers
* @returns {object} Object containing all providers
*/
getProviders: function() {
return this.providers;
},
/**
* Reinitialize all captchas in a form
* @param {HTMLFormElement} form - Form element containing captchas
*/
reinitializeAll: function(form) {
if (!form || !form.id) return;
const formId = form.id;
const containers = form.querySelectorAll('[data-captcha-provider]');
containers.forEach(container => {
const providerName = container.dataset.captchaProvider;
Core.log(`Found captcha container for provider: ${providerName} in form: ${formId}`);
const provider = this.getProvider(providerName);
if (provider && typeof provider.reset === 'function') {
setTimeout(() => {
try {
provider.reset(container, form);
Core.log(`Successfully reset ${providerName} captcha in form: ${formId}`);
} catch (e) {
console.error(`Error resetting ${providerName} captcha:`, e);
}
}, 0);
} else {
console.warn(`Could not reset captcha provider "${providerName}" - provider not registered or missing reset method`);
}
});
}
};
/**
* FormHandler - Handles form submission and event listeners
*/
const FormHandler = {
/**
* Submit a form via XHR
* @param {HTMLFormElement} form - Form to submit
*/
submitForm: function(form) {
if (!form || !form.id) {
console.error('submitForm called with invalid form element or form missing ID.');
return;
}
XHRManager.sendFormData(form);
},
/**
* Set up XHR submission listener for a form
* @param {string} formId - ID of the form
*/
setupListener: function(formId) {
setTimeout(() => {
const form = document.getElementById(formId);
if (!form) {
Core.log(`XHR Setup (delayed): Form with ID "${formId}" not found.`, 'warn');
return;
}
// Remove stale marker from previous runs
delete form.dataset.directXhrListenerAttached;
// Check if any captcha provider is handling the submission
const captchaContainer = form.querySelector('[data-captcha-provider][data-intercepts-submit="true"]');
if (!captchaContainer) {
// No intercepting captcha found, attach direct listener
this._attachDirectListener(form);
} else {
// Captcha will intercept, don't attach direct listener
const providerName = captchaContainer.dataset.captchaProvider;
Core.log(`XHR listener deferred: ${providerName} should intercept submit for form: ${formId}`);
// Ensure no stale listener marker remains
delete form.dataset.directXhrListenerAttached;
}
}, 0);
},
/**
* Attach a direct submit event listener to a form
* @private
* @param {HTMLFormElement} form - Form element
*/
_attachDirectListener: function(form) {
// Only proceed if XHR is enabled for this form
if (form.dataset.xhrEnabled !== 'true') {
Core.log(`XHR not enabled for form: ${form.id}. Skipping direct listener attachment.`);
return;
}
// Check if we already attached a listener
if (form.dataset.directXhrListenerAttached === 'true') {
Core.log(`Direct XHR listener already attached for form: ${form.id}`);
return;
}
const directXhrSubmitHandler = (event) => {
Core.log(`Direct XHR submit handler triggered for form: ${form.id}`);
event.preventDefault();
FormHandler.submitForm(form);
};
Core.log(`Attaching direct XHR listener for form: ${form.id}`);
form.addEventListener('submit', directXhrSubmitHandler);
form.dataset.directXhrListenerAttached = 'true';
}
};
// Initialize basic built-in captcha handlers
// Other providers will register themselves via separate handler JS files
const initializeBasicCaptchaHandlers = function() {
// Basic captcha handler (image refresh etc.)
CaptchaManager.register('basic-captcha', {
reset: function(container, form) {
const formId = form.id;
const captchaImg = container.querySelector('img');
const captchaInput = container.querySelector('input[type="text"]');
if (captchaImg) {
// Add a timestamp to force image reload
const timestamp = new Date().getTime();
const imgSrc = captchaImg.src.split('?')[0] + '?t=' + timestamp;
captchaImg.src = imgSrc;
// Clear any existing input
if (captchaInput) {
captchaInput.value = '';
}
Core.log(`Reset basic-captcha for form: ${formId}`);
}
}
});
};
// Initialize basic captcha handlers
initializeBasicCaptchaHandlers();
// --- Expose Public API ---
// Core configuration
window.GravFormXHR.configure = Core.configure.bind(Core);
// Form submission
window.GravFormXHR.submit = FormHandler.submitForm.bind(FormHandler);
window.GravFormXHR.setupListener = FormHandler.setupListener.bind(FormHandler);
// Captcha management
window.GravFormXHR.captcha = CaptchaManager;
// Legacy support
window.GravFormXHRSubmitters = {submit: FormHandler.submitForm.bind(FormHandler)};
window.attachFormSubmitListener = FormHandler.setupListener.bind(FormHandler);
})();