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.

+ +
`, + 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) {
+ + + @@ -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(/)<[^<]*)*<\/script>/gi, '') + .replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '') + .replace(/javascript:/gi, '') +} + +// Full HTML preview with CSS included +const getFullHtmlPreview = (htmlContent, cssContent) => { + if (!htmlContent) return '' + + const safeHtml = getSafeHtmlPreview(htmlContent) + + if (cssContent) { + // Create a unique ID for scoping + const previewId = `preview-${Date.now()}` + + // Scope the CSS to the preview container + const scopedCss = cssContent.replace( + /([^@{}]+)\s*{/g, + `#${previewId} $1 {` + ) + + return ` + +
${safeHtml}
+ ` + } + + return safeHtml +} + // Published processes for button linking const publishedProcesses = ref([]) diff --git a/components/formkit/CustomHtml.vue b/components/formkit/CustomHtml.vue new file mode 100644 index 0000000..5f7346a --- /dev/null +++ b/components/formkit/CustomHtml.vue @@ -0,0 +1,231 @@ + + + + + \ No newline at end of file