diff --git a/components/ComponentPreview.vue b/components/ComponentPreview.vue
index 8503ff0..f5a85a7 100644
--- a/components/ComponentPreview.vue
+++ b/components/ComponentPreview.vue
@@ -47,9 +47,6 @@
:multiple="component.props.multiple || undefined"
:maxSize="component.props.maxSize || undefined"
:maxFiles="component.props.maxFiles || undefined"
- :preserve-events="isPreview"
- @input.capture.stop="isPreview ? $event.stopPropagation() : null"
- @click.capture.stop="isPreview ? $event.stopPropagation() : null"
:classes="component.type === 'checkbox' ? {
wrapper: 'mb-1',
options: 'space-y-0.5'
@@ -124,7 +121,7 @@
-
+
@@ -349,7 +346,7 @@
-
+
Unknown component type: {{ component.type }}
diff --git a/components/FormBuilderConfiguration.vue b/components/FormBuilderConfiguration.vue
index 7085561..03de8e3 100644
--- a/components/FormBuilderConfiguration.vue
+++ b/components/FormBuilderConfiguration.vue
@@ -446,8 +446,8 @@
name="maxFiles"
v-model="configModel.maxFiles"
help="Maximum number of files"
- min="1"
- />
+ min="1"
+ />
diff --git a/components/FormScriptEngine.vue b/components/FormScriptEngine.vue
index c765a3e..bffc849 100644
--- a/components/FormScriptEngine.vue
+++ b/components/FormScriptEngine.vue
@@ -1,7 +1,7 @@
-
-
+
+
{{ customCSS }}
@@ -44,16 +44,31 @@ const emit = defineEmits(['field-change', 'form-submit', 'field-validate']);
const scriptContext = ref(null);
const fieldChangeHandlers = ref(new Map());
const isScriptInitialized = ref(false);
+const previousFormData = ref({});
// Create safe execution context
const createScriptContext = () => {
const context = {
- // Form field interaction methods
getField: (fieldName) => {
return props.formData[fieldName];
},
setField: (fieldName, value) => {
+ // Try to find the FormKit input element and update it directly
+ const fieldElement = document.querySelector(`[data-name="${fieldName}"] input, [data-name="${fieldName}"] select, [data-name="${fieldName}"] textarea`);
+ if (fieldElement) {
+ fieldElement.value = value;
+
+ // Trigger input event to notify FormKit of the change
+ const inputEvent = new Event('input', { bubbles: true });
+ fieldElement.dispatchEvent(inputEvent);
+
+ // Also trigger change event
+ const changeEvent = new Event('change', { bubbles: true });
+ fieldElement.dispatchEvent(changeEvent);
+ }
+
+ // Also emit the event for the parent component
emit('field-change', { fieldName, value });
},
@@ -107,6 +122,46 @@ const createScriptContext = () => {
});
},
+ // Add missing helper functions
+ showSuccess: (message) => {
+ // Create a simple success notification
+ const notification = document.createElement('div');
+ notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow-lg z-50';
+ notification.textContent = message;
+ document.body.appendChild(notification);
+ setTimeout(() => {
+ if (document.body.contains(notification)) {
+ document.body.removeChild(notification);
+ }
+ }, 3000);
+ },
+
+ showError: (message) => {
+ // Create a simple error notification
+ const notification = document.createElement('div');
+ notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded shadow-lg z-50';
+ notification.textContent = message;
+ document.body.appendChild(notification);
+ setTimeout(() => {
+ if (document.body.contains(notification)) {
+ document.body.removeChild(notification);
+ }
+ }, 5000);
+ },
+
+ showInfo: (message) => {
+ // Create a simple info notification
+ const notification = document.createElement('div');
+ notification.className = 'fixed top-4 right-4 bg-blue-500 text-white px-4 py-2 rounded shadow-lg z-50';
+ notification.textContent = message;
+ document.body.appendChild(notification);
+ setTimeout(() => {
+ if (document.body.contains(notification)) {
+ document.body.removeChild(notification);
+ }
+ }, 3000);
+ },
+
// Utility functions
console: {
log: (...args) => console.log('[Form Script]', ...args),
@@ -134,7 +189,13 @@ const createScriptContext = () => {
String: String,
// Number utilities
- Number: Number
+ Number: Number,
+
+ // Additional utility functions
+ setTimeout: setTimeout,
+ setInterval: setInterval,
+ clearTimeout: clearTimeout,
+ clearInterval: clearInterval
};
return context;
@@ -183,20 +244,26 @@ const executeScript = (script, context) => {
scriptFunction.call(null, context);
}
} catch (error) {
- console.error('Script execution error:', error);
- // Could emit an error event here for user feedback
+ console.error('[FormScriptEngine] Script execution error:', error);
+ // Show error to user if showError function is available
+ if (context.showError) {
+ context.showError(`Script Error: ${error.message}`);
+ }
}
};
// Initialize script engine
-const initializeScript = async () => {
- if (!props.customScript || isScriptInitialized.value) return;
-
- await nextTick(); // Ensure DOM is ready
+const initializeScript = () => {
+ fieldChangeHandlers.value.clear();
+ // Create script context
scriptContext.value = createScriptContext();
- if (props.formEvents.onLoad) {
+ // Store initial form data
+ previousFormData.value = { ...props.formData };
+
+ // Execute onLoad script if enabled
+ if (props.formEvents.onLoad && props.customScript) {
executeScript(props.customScript, scriptContext.value);
}
@@ -205,44 +272,63 @@ const initializeScript = async () => {
// Handle field changes
const handleFieldChange = (fieldName, newValue, oldValue) => {
- if (!fieldChangeHandlers.value.has(fieldName)) return;
+ if (!fieldChangeHandlers.value.has(fieldName)) {
+ return;
+ }
const handlers = fieldChangeHandlers.value.get(fieldName);
- handlers.forEach(handler => {
+ handlers.forEach((handler, index) => {
try {
if (typeof handler === 'function') {
handler.call(scriptContext.value, newValue, oldValue);
}
} catch (error) {
- console.error(`Error in field change handler for '${fieldName}':`, error);
+ console.error(`[FormScriptEngine] Error in onFieldChange handler for "${fieldName}":`, error);
+ if (scriptContext.value?.showError) {
+ scriptContext.value.showError(`Field change handler error for ${fieldName}: ${error.message}`);
+ }
}
});
};
-// Watch for form data changes
-watch(() => props.formData, (newData, oldData) => {
- if (!isScriptInitialized.value || !props.formEvents.onFieldChange) return;
-
- // Compare and trigger handlers for changed fields
- Object.keys(newData).forEach(fieldName => {
- if (newData[fieldName] !== oldData?.[fieldName]) {
- handleFieldChange(fieldName, newData[fieldName], oldData?.[fieldName]);
- }
- });
-}, { deep: true });
-
// Watch for script changes and reinitialize
-watch(() => props.customScript, () => {
- if (isScriptInitialized.value) {
+watch(() => props.customScript, (newScript, oldScript) => {
+ if (newScript !== oldScript) {
isScriptInitialized.value = false;
- fieldChangeHandlers.value.clear();
initializeScript();
}
});
+// Watch for form data changes - the elegant, performant way
+watch(() => props.formData, (newData, oldData) => {
+ if (!isScriptInitialized.value || !props.formEvents.onFieldChange) {
+ return;
+ }
+
+ // Compare with our stored previousFormData to detect changes
+ Object.keys(newData).forEach(fieldName => {
+ const newValue = newData[fieldName];
+ const oldValue = previousFormData.value[fieldName];
+
+ if (newValue !== oldValue) {
+ handleFieldChange(fieldName, newValue, oldValue);
+ }
+ });
+
+ // Update previousFormData for next comparison
+ previousFormData.value = { ...newData };
+}, { deep: true, immediate: false });
+
+// Watch for CSS changes and update
+watch(() => props.customCSS, () => {
+ // CSS will be automatically updated by the template reactivity
+});
+
// Lifecycle hooks
onMounted(() => {
- initializeScript();
+ setTimeout(() => {
+ initializeScript();
+ }, 500); // Delay to ensure parent DOM and initial props are settled
});
onUnmounted(() => {
@@ -259,13 +345,12 @@ defineExpose({
},
getContext: () => scriptContext.value,
reinitialize: () => {
- isScriptInitialized.value = false;
- fieldChangeHandlers.value.clear();
- initializeScript();
+ isScriptInitialized.value = false;
+ initializeScript();
}
});
-
\ No newline at end of file
diff --git a/components/FormTemplatesModal.vue b/components/FormTemplatesModal.vue
index ee8bda2..4337667 100644
--- a/components/FormTemplatesModal.vue
+++ b/components/FormTemplatesModal.vue
@@ -141,6 +141,210 @@ const activeCategory = ref('all');
// Templates data
const templates = [
+ {
+ id: 'test-css-js',
+ name: 'CSS & JavaScript Test',
+ description: 'Simple test form to verify custom CSS and JavaScript functionality works correctly.',
+ category: 'advanced',
+ image: null,
+ hasScript: true,
+ hasCSS: true,
+ componentCount: 6,
+ components: [
+ {
+ type: 'heading',
+ name: 'Heading',
+ category: 'Layout',
+ icon: 'material-symbols:title',
+ defaultProps: {
+ value: 'CSS & JavaScript Test Form',
+ level: 1,
+ width: '100%',
+ gridColumn: 'span 12'
+ }
+ },
+ {
+ type: 'paragraph',
+ name: 'Paragraph',
+ category: 'Layout',
+ icon: 'material-symbols:text-snippet-outline',
+ defaultProps: {
+ value: 'This form tests custom CSS styling and JavaScript functionality. Try changing the values to see the scripts in action!',
+ width: '100%',
+ gridColumn: 'span 12'
+ }
+ },
+ {
+ type: 'text',
+ name: 'Text Field',
+ category: 'Basic Inputs',
+ icon: 'material-symbols:text-fields',
+ defaultProps: {
+ type: 'text',
+ label: 'Your Name',
+ name: 'user_name',
+ placeholder: 'Enter your name',
+ help: 'This field will trigger JavaScript when changed',
+ validation: 'required',
+ width: '50%',
+ gridColumn: 'span 6'
+ }
+ },
+ {
+ type: 'number',
+ name: 'Number',
+ category: 'Basic Inputs',
+ icon: 'material-symbols:counter-1-outline',
+ defaultProps: {
+ type: 'number',
+ label: 'Quantity',
+ name: 'quantity',
+ placeholder: '1',
+ help: 'Change this to see the total calculation',
+ min: 1,
+ max: 100,
+ validation: 'required',
+ width: '25%',
+ gridColumn: 'span 3'
+ }
+ },
+ {
+ type: 'number',
+ name: 'Number',
+ category: 'Basic Inputs',
+ icon: 'material-symbols:counter-1-outline',
+ defaultProps: {
+ type: 'number',
+ label: 'Price ($)',
+ name: 'price',
+ placeholder: '10.00',
+ help: 'Price per item',
+ min: 0,
+ step: 0.01,
+ validation: 'required',
+ width: '25%',
+ gridColumn: 'span 3'
+ }
+ },
+ {
+ type: 'text',
+ name: 'Text Field',
+ category: 'Basic Inputs',
+ icon: 'material-symbols:text-fields',
+ defaultProps: {
+ type: 'text',
+ label: 'Total Amount',
+ name: 'total',
+ placeholder: '$0.00',
+ help: 'This will be calculated automatically',
+ width: '100%',
+ gridColumn: 'span 12'
+ }
+ }
+ ],
+ script: `// Welcome message when form loads
+this.showSuccess('CSS & JavaScript test form loaded successfully!');
+
+// Greet user when name changes
+this.onFieldChange('user_name', (value) => {
+ if (value && value.trim()) {
+ this.showInfo('Hello, ' + value.trim() + '! Welcome to the test form.');
+ }
+});
+
+// Calculate total when quantity or price changes
+this.onFieldChange(['quantity', 'price'], () => {
+ const quantity = parseFloat(this.getField('quantity')) || 0;
+ const price = parseFloat(this.getField('price')) || 0;
+ const total = quantity * price;
+
+ this.setField('total', '$' + total.toFixed(2));
+
+ if (total > 0) {
+ this.console.log('Total calculated:', total);
+
+ if (total > 50) {
+ this.showSuccess('Great! Total is over $50');
+ } else if (total > 100) {
+ this.showInfo('Wow! Total is over $100');
+ }
+ }
+});
+
+// Initialize with default values when form loads
+setTimeout(() => {
+ this.setField('quantity', 2);
+ this.setField('price', 15.50);
+}, 500);`,
+ css: `.form-container {
+ background: linear-gradient(135deg, #fef7ff 0%, #f3e8ff 100%);
+ border: 3px solid #a855f7;
+ border-radius: 20px;
+ padding: 2rem;
+ box-shadow: 0 20px 40px rgba(168, 85, 247, 0.2);
+}
+
+/* Style the name field with a special border */
+.form-field[data-name="user_name"] {
+ background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
+ border: 2px solid #10b981;
+ border-radius: 12px;
+ padding: 1rem;
+}
+
+.form-field[data-name="user_name"] input {
+ background: transparent;
+ border: none;
+ font-weight: 600;
+ color: #065f46;
+}
+
+/* Style the quantity field */
+.form-field[data-name="quantity"] {
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+ border: 2px solid #f59e0b;
+ border-radius: 12px;
+ padding: 1rem;
+}
+
+/* Style the price field */
+.form-field[data-name="price"] {
+ background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+ border: 2px solid #3b82f6;
+ border-radius: 12px;
+ padding: 1rem;
+}
+
+/* Style the total field with special emphasis */
+.form-field[data-name="total"] {
+ background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
+ border: 3px solid #ef4444;
+ border-radius: 16px;
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.form-field[data-name="total"] input {
+ background: transparent;
+ border: none;
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #dc2626;
+ text-align: center;
+}
+
+/* Add some hover effects */
+.form-field:hover {
+ transform: translateY(-2px);
+ transition: all 0.3s ease;
+}
+
+/* Style the heading */
+h1 {
+ color: #7c3aed;
+ text-shadow: 2px 2px 4px rgba(124, 58, 237, 0.3);
+}`
+ },
{
id: 'contact-form',
name: 'Contact Form',
@@ -2187,6 +2391,7 @@ this.onFieldChange('budget', (value) => {
// Update display somewhere in the form
console.log('Budget formatted:', formatted);
+ this.showInfo('Budget updated: ' + formatted);
});
// OTP validation
@@ -2201,18 +2406,32 @@ this.onFieldChange('otp_code', (value) => {
}
}, 1000);
}
-});`,
+});
+
+// Initialize form
+console.log('Advanced form script loaded successfully!');`,
css: `.form-container {
- background: #f8fafc;
- border-radius: 12px;
+ background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 100%);
+ border-radius: 16px;
padding: 2rem;
- border: 1px solid #e2e8f0;
+ border: 2px solid #e2e8f0;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+}
+
+.form-field[data-name="budget"] {
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+ border: 2px solid #f59e0b;
+ border-radius: 12px;
+ padding: 1rem;
+ font-weight: bold;
+ font-size: 1.1rem;
}
.form-field[data-name="info_display"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.form-field[data-name="dropzone"] {
@@ -2225,6 +2444,8 @@ this.onFieldChange('otp_code', (value) => {
.form-field[data-name="dropzone"]:hover {
border-color: #3b82f6;
background: #eff6ff;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.form-field[data-name="otp_code"] input {
@@ -2232,6 +2453,19 @@ this.onFieldChange('otp_code', (value) => {
font-family: monospace;
font-size: 1.2rem;
letter-spacing: 0.5rem;
+ background: #f0f9ff;
+ border: 2px solid #3b82f6;
+}
+
+/* Animated submit button */
+.form-field[data-name="submit_advanced"] button {
+ background: linear-gradient(45deg, #3b82f6, #1d4ed8);
+ transition: all 0.3s ease;
+}
+
+.form-field[data-name="submit_advanced"] button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
}`
}
];
diff --git a/components/RsModal.vue b/components/RsModal.vue
index 231be34..5fda7f8 100644
--- a/components/RsModal.vue
+++ b/components/RsModal.vue
@@ -117,8 +117,8 @@ watch(
>
-
-
+
+
@@ -598,8 +645,18 @@
Form Events
- Configure when your custom scripts should run.
+ Configure when your custom scripts should run. Field change events are detected automatically in real-time when users interact with form fields.
+
+
+
+
+
+
+ Note: The "On Field Change" event must be enabled for onFieldChange()
callbacks to work in your scripts.
+
+
+
@@ -627,14 +684,20 @@
Script Execution Mode
-
@@ -796,6 +859,7 @@