diff --git a/assets/js/formkit-custom.js b/assets/js/formkit-custom.js
index adaa0bd..11c2c22 100644
--- a/assets/js/formkit-custom.js
+++ b/assets/js/formkit-custom.js
@@ -4,6 +4,7 @@ import MaskText from "~/components/formkit/TextMask.vue";
import FileDropzone from "~/components/formkit/FileDropzone.vue";
import Switch from "~/components/formkit/Switch.vue";
import SearchSelect from "~/components/formkit/SearchSelect.vue";
+import CustomHtml from "~/components/formkit/CustomHtml.vue";
export default {
otp: createInput(OneTimePassword, {
@@ -21,4 +22,7 @@ export default {
searchSelect: createInput(SearchSelect, {
props: ["options", "placeholder"],
}),
+ customHtml: createInput(CustomHtml, {
+ props: ["htmlContent", "cssContent", "jsContent", "allowScripts", "previewMode", "showInPreview"],
+ }),
};
diff --git a/components/ComponentPreview.vue b/components/ComponentPreview.vue
index ef287c2..b906ef7 100644
--- a/components/ComponentPreview.vue
+++ b/components/ComponentPreview.vue
@@ -627,6 +627,24 @@
+
+
+
+
+
Unknown component type: {{ component.type }}
diff --git a/components/FormBuilderComponents.vue b/components/FormBuilderComponents.vue
index 3d06ce4..157689c 100644
--- a/components/FormBuilderComponents.vue
+++ b/components/FormBuilderComponents.vue
@@ -732,6 +732,78 @@ const availableComponents = [
icon: 'material-symbols:horizontal-rule',
description: 'Horizontal divider line',
defaultProps: {}
+ },
+ {
+ type: 'customHtml',
+ name: 'Custom HTML',
+ category: 'Layout',
+ icon: 'material-symbols:code',
+ description: 'Custom HTML with CSS and JavaScript',
+ defaultProps: {
+ label: 'Custom HTML Component',
+ name: 'custom_html',
+ help: 'Create custom designs using HTML, CSS, and JavaScript',
+ htmlContent: `
+
Custom HTML Component
+
Edit this HTML to create your custom design.
+
Click me!
+
`,
+ cssContent: `.custom-component {
+ padding: 20px;
+ border: 2px solid #3b82f6;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+ text-align: center;
+}
+
+.custom-component h3 {
+ color: #1e40af;
+ margin-bottom: 10px;
+}
+
+.custom-component button {
+ background: #3b82f6;
+ color: white;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-top: 10px;
+}
+
+.custom-component button:hover {
+ background: #2563eb;
+}`,
+ jsContent: `// Custom JavaScript for this component
+// You can access the component element via 'this.element'
+// Available functions:
+// - this.element: The HTML element of this component
+// - this.getValue(): Get form values
+// - this.setValue(name, value): Set form field values
+
+console.log('Custom HTML component initialized');
+
+// Example: Add click handler
+const buttons = this.element.querySelectorAll('button');
+buttons.forEach(button => {
+ button.addEventListener('click', function(e) {
+ e.preventDefault();
+ console.log('Custom button clicked!');
+ });
+});`,
+ allowScripts: true,
+ previewMode: 'safe', // 'safe' or 'advanced'
+ width: '100%',
+ gridColumn: 'span 12',
+ showInPreview: true,
+ // Conditional Logic Properties
+ conditionalLogic: {
+ enabled: false,
+ conditions: [],
+ action: 'show',
+ operator: 'and'
+ }
+ }
}
];
diff --git a/components/FormBuilderFieldSettingsModal.vue b/components/FormBuilderFieldSettingsModal.vue
index b004a9a..61b6b57 100644
--- a/components/FormBuilderFieldSettingsModal.vue
+++ b/components/FormBuilderFieldSettingsModal.vue
@@ -1575,6 +1575,136 @@ if (name && email) {
+
+
+
+
+
+
+
HTML Content
+
+
+
+
+
+
+
CSS Styles
+
+
+
+
+
+
+
JavaScript Code
+
+
+
+
+
+
+
Component Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
Live Preview
+
+
+
+
+
+
No HTML content to preview
+
+
+
+
+
+
+
+
+
+
Security Notice
+
+ Custom HTML components can contain arbitrary code. In Safe Mode, scripts and event handlers are disabled.
+ Use Advanced Mode only with trusted content and enable "Allow Scripts" carefully.
+
+
+
+
+
+
@@ -2027,7 +2157,8 @@ const getComponentIcon = (type) => {
'info-display': 'heroicons:information-circle',
'dynamic-list': 'heroicons:list-bullet',
'repeating-table': 'heroicons:table-cells',
- 'repeating-group': 'heroicons:list-bullet'
+ 'repeating-group': 'heroicons:list-bullet',
+ 'customHtml': 'material-symbols:code'
}
return icons[type] || 'heroicons:square-3-stack-3d'
}
@@ -2064,7 +2195,8 @@ const getComponentTypeName = (type) => {
'info-display': 'Information Display',
'dynamic-list': 'Dynamic List',
'repeating-table': 'Data Table',
- 'repeating-group': 'Repeating Group'
+ 'repeating-group': 'Repeating Group',
+ 'customHtml': 'Custom HTML'
}
return names[type] || 'Form Field'
}
@@ -2101,7 +2233,8 @@ const getComponentDescription = (type) => {
'info-display': 'Read-only information display in organized format',
'dynamic-list': 'Dynamic list for displaying and managing items',
'repeating-table': 'Structured table for collecting multiple records with forms',
- 'repeating-group': 'Collect multiple entries of the same data structure'
+ 'repeating-group': 'Collect multiple entries of the same data structure',
+ 'customHtml': 'Create custom designs using HTML, CSS, and JavaScript'
}
return descriptions[type] || 'Configure this form field'
}
@@ -2129,15 +2262,15 @@ const showField = (fieldName) => {
if (!props.component) return false
const fieldConfig = {
- label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
- name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
+ label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
+ name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'dynamic-list'],
- help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
+ help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
value: ['heading', 'paragraph', 'hidden'],
- width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'form-section', 'info-display', 'dynamic-list', 'repeating-table', 'repeating-group'],
+ width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'form-section', 'info-display', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
rows: ['textarea'],
options: ['select', 'searchSelect', 'checkbox', 'radio'],
- conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
+ conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
readonly: ['text', 'number', 'email', 'password', 'textarea', 'mask', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch']
}
@@ -2147,7 +2280,7 @@ const showField = (fieldName) => {
const hasOptions = computed(() => showField('options'))
const hasSpecificSettings = computed(() => {
if (!props.component) return false
- const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group']
+ const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml']
return specificTypes.includes(props.component.type)
})
@@ -2190,6 +2323,48 @@ const isTextBasedField = computed(() => {
return ['text', 'textarea', 'email', 'password', 'url', 'tel'].includes(props.component?.type)
})
+// Safe HTML preview for custom HTML component
+const getSafeHtmlPreview = (htmlContent) => {
+ if (!htmlContent) return ''
+
+ // Remove script tags and event handlers for safe preview
+ return htmlContent
+ .replace(/
+
+
+
+
+
+ {{ context.label }}
+
+
+
+
+ {{ context.help }}
+
+
+
+
+
+ Safe Mode: Scripts and event handlers are disabled for security.
+
+
+
+
+
+
+
+
+
+
+ HTML: {{ htmlContent.length }} chars
+
+
+
+ CSS: {{ cssContent.length }} chars
+
+
+
+ JS: {{ jsContent.length }} chars
+
+
+
+
+
+
+
\ No newline at end of file