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:
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
166
config/www/user/plugins/form/assets/captcha/recaptcha-handler.js
Normal file
166
config/www/user/plugins/form/assets/captcha/recaptcha-handler.js
Normal 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);
|
||||
});
|
||||
}
|
||||
})();
|
||||
121
config/www/user/plugins/form/assets/captcha/turnstile-handler.js
Normal file
121
config/www/user/plugins/form/assets/captcha/turnstile-handler.js
Normal 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);
|
||||
});
|
||||
}
|
||||
})();
|
||||
311
config/www/user/plugins/form/assets/dropzone-reinit.js
Normal file
311
config/www/user/plugins/form/assets/dropzone-reinit.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
1
config/www/user/plugins/form/assets/dropzone.min.css
vendored
Normal file
1
config/www/user/plugins/form/assets/dropzone.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
662
config/www/user/plugins/form/assets/filepond-handler.js
Normal file
662
config/www/user/plugins/form/assets/filepond-handler.js
Normal 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');
|
||||
})();
|
||||
141
config/www/user/plugins/form/assets/filepond-reinit.js
Normal file
141
config/www/user/plugins/form/assets/filepond-reinit.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-size.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-size.min.js
vendored
Normal 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});
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-type.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-type.min.js
vendored
Normal 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});
|
||||
8
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.css
vendored
Normal file
8
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.css
vendored
Normal 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}
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-resize.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-resize.min.js
vendored
Normal 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});
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-transform.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-transform.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
config/www/user/plugins/form/assets/filepond/filepond.min.css
vendored
Normal file
8
config/www/user/plugins/form/assets/filepond/filepond.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
config/www/user/plugins/form/assets/filepond/filepond.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
config/www/user/plugins/form/assets/form-styles.css
Normal file
1
config/www/user/plugins/form/assets/form-styles.css
Normal 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 */
|
||||
1
config/www/user/plugins/form/assets/form-styles.css.map
Normal file
1
config/www/user/plugins/form/assets/form-styles.css.map
Normal 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"}
|
||||
1
config/www/user/plugins/form/assets/form-styles.min.css
vendored
Normal file
1
config/www/user/plugins/form/assets/form-styles.min.css
vendored
Normal 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}
|
||||
1
config/www/user/plugins/form/assets/form.min.js
vendored
Normal file
1
config/www/user/plugins/form/assets/form.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
config/www/user/plugins/form/assets/form.vendor.js
Normal file
1
config/www/user/plugins/form/assets/form.vendor.js
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
});
|
||||
}
|
||||
617
config/www/user/plugins/form/assets/signature_pad.js
Executable file
617
config/www/user/plugins/form/assets/signature_pad.js
Executable 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;
|
||||
|
||||
})));
|
||||
461
config/www/user/plugins/form/assets/xhr-submitter.js
Normal file
461
config/www/user/plugins/form/assets/xhr-submitter.js
Normal 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);
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user