PluginProbe
FormLayer / 1.0.7
FormLayer v1.0.7
1.0.9 1.0.8 1.0.7 1.0.6 trunk 1.0.3 1.0.4 1.0.5
formlayer / assets / js / admin.js

admin.js in FormLayer 1.0.7, at assets/js/admin.js

1,550 lines 61.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function($){
2
3 // Toast Notification
4 function show_toast(message, type = 'success') {
5 let toast = $('<div>')
6 .addClass('formlayer-toast')
7 .addClass(type)
8 .html(`<span class="dashicons dashicons-${type === 'success' ? 'yes' : 'warning'}"></span> ${message}`);
9
10 $('body').append(toast);
11
12 toast.fadeIn(300).delay(3000).fadeOut(300, function () {
13 toast.remove();
14 });
15 }
16
17 /**
18 * FormLayer Admin SPA Tab Handling
19 */
20 function handle_tabs(){
21 let hash_full = window.location.hash.trim().replace('#', '');
22 let hash = hash_full.split('/')[0];
23 let $nav = $('#formlayer-main-nav');
24
25 // Handle hash or query param
26 if(!hash){
27 let url_params = new URLSearchParams(window.location.search);
28 hash = url_params.get('tab') || 'forms';
29 }
30
31 let $target_tab = $('#formlayer-tab-' + hash),
32 $nav_item = $nav.find(`a[data-tab="${hash}"]`);
33
34 if($target_tab.length){
35 // Hide all tabs
36 $('.formlayer-tab-content').hide();
37 // Show target tab
38 $target_tab.fadeIn(200);
39
40 // Update nav state
41 $nav.find('.formlayer-tab-item').removeClass('active');
42 $nav_item.addClass('active');
43
44 // Special handling for builder layout
45 if(hash === 'formbuilder'){
46 $('.formlayer-admin-wrapper').addClass('formlayer-builder-active');
47 }else{
48 $('.formlayer-admin-wrapper').removeClass('formlayer-builder-active');
49
50 // Remove form_id from URL
51 const url = new URL(window.location.href);
52 if(url.searchParams.has('form_id')){
53 url.searchParams.delete('form_id');
54 window.history.replaceState({}, document.title, url.toString());
55 if(typeof formlayer_admin !== 'undefined'){
56 formlayer_admin.form_id = 0;
57 }
58 }
59 }
60 }else if(hash === 'forms'){
61 // Fallback for empty hash or 'forms'
62 $('.formlayer-tab-content').hide();
63 $('#formlayer-tab-forms').show();
64 $nav.find('.formlayer-tab-item').removeClass('active');
65 $nav.find('a[data-tab="forms"]').addClass('active');
66 }
67 }
68
69 // Tab change listener
70 $(window).on('hashchange', function(){
71 handle_tabs();
72 });
73
74 // Add New Form
75 $('#formlayer-add-new-form').on('click', function(e){
76 e.preventDefault();
77 state.view = 'templates';
78 state.fields = [];
79 state.form_title = 'Untitled Form';
80 formlayer_admin.form_id = 0;
81 window.location.hash = 'formbuilder';
82 render();
83 });
84
85 // Initial tab load
86 handle_tabs();
87
88 // Move all modals to body to escape transformed containers and center correctly
89 $('.formlayer-modal-overlay').appendTo('body');
90
91 // Select All handler
92 $('#formlayer-select-all').on('change', function(){
93 $('.formlayer-row-cb').prop('checked', $(this).prop('checked'));
94 });
95
96 $('.formlayer-row-cb').on('change', function(){
97 if($('.formlayer-row-cb:checked').length === $('.formlayer-row-cb').length){
98 $('#formlayer-select-all').prop('checked', true);
99 }else{
100 $('#formlayer-select-all').prop('checked', false);
101 }
102 });
103
104 // Bulk Action handler
105 $('#formlayer-apply-bulk').on('click', function(e){
106 e.preventDefault();
107 let action = $('#formlayer-bulk-action').val();
108 let selected_ids = $('.formlayer-row-cb:checked').map(function(){
109 return $(this).val();
110 }).get();
111
112 if(!action){
113 show_toast('Please select an action.', 'error');
114 return;
115 }
116
117 if(selected_ids.length === 0){
118 show_toast('Please select at least one form.', 'error');
119 return;
120 }
121
122 if(!confirm(`Are you sure you want to ${action} selected forms?`)){
123 return;
124 }
125
126 let $btn = $(this);
127 $btn.prop('disabled', true).text('Applying...');
128
129 $.post(formlayer_admin.ajax_url, {
130 action: 'formlayer_bulk_action',
131 bulk_action: action,
132 ids: selected_ids,
133 nonce: formlayer_admin.nonce
134 }, function(response){
135 if(response.success){
136 location.reload();
137 }else{
138 show_toast(response.data.message || 'Error applying bulk action', 'error');
139 $btn.prop('disabled', false).text('Apply');
140 }
141 });
142 });
143
144 // Save Settings handler
145 $('#formlayer-save-settings').on('click', function(e){
146 e.preventDefault();
147 let $btn = $(this);
148 $status = $('#formlayer-settings-status'),
149 original_text = $btn.text();
150
151 let settings = {};
152 $('input[name], select[name], textarea[name]').each(function(){
153 let name = $(this).attr('name');
154 if($(this).attr('type') === 'checkbox'){
155 settings[name] = $(this).is(':checked') ? '1' : '0';
156 }else{
157 settings[name] = $(this).val();
158 }
159 });
160
161 $btn.prop('disabled', true).text('Saving...');
162 if($status.length) $status.removeClass('success error').empty();
163
164 $.post(formlayer_admin.ajax_url, {
165 action: 'formlayer_save_settings',
166 settings: settings,
167 nonce: formlayer_admin.nonce
168 }, function(response){
169 if(response.success){
170 show_toast('Settings saved successfully!', 'success');
171 $btn.prop('disabled', false).text(original_text);
172 }else{
173 show_toast(response.data.message || 'Error saving settings', 'error');
174 $btn.prop('disabled', false).text(original_text);
175 }
176 });
177 });
178
179 // Captcha Tab Switching
180 $('.captcha-tab-item').on('click', function(){
181 let target = $(this).data('target');
182 $('.captcha-tab-item').removeClass('active');
183 $(this).addClass('active');
184
185 $('.captcha-pane').removeClass('active');
186 $(`#pane-${target}`).addClass('active');
187 });
188
189 // Delete form handler
190 $('.formlayer-delete-form').on('click', function(e){
191 e.preventDefault();
192 var form_id = $(this).data('form-id');
193 if(!form_id) return;
194
195 if(!confirm('Are you sure you want to delete this form? This action cannot be undone.')){
196 return;
197 }
198
199 var $btn = $(this);
200 $btn.css('opacity', '0.5');
201
202 $.post(formlayer_admin.ajax_url, {
203 action: 'formlayer_delete_form',
204 form_id: form_id,
205 nonce: formlayer_admin.nonce
206 }, function(response){
207 if(response.success){
208 show_toast('Form deleted successfully!', 'success');
209 $btn.closest('tr').fadeOut(300, function(){
210 $(this).remove();
211 // If no rows left, show "No forms found"
212 let $tbody = $('#formlayer-tab-forms .formlayer-table tbody');
213 if ($tbody.find('tr').length === 0) {
214 $tbody.html(`
215 <tr>
216 <td colspan="7" style="text-align:center; padding:60px 0; color:var(--formlayer-text-muted);">
217 <div class="formlayer-empty-title">No forms found</div>
218 <p style="margin:0;">Create your first form to start collecting data.</p>
219 </td>
220 </tr>
221 `);
222 }
223 });
224 }else{
225 show_toast(response.data.message || 'Error deleting form', 'error');
226 $btn.css('opacity', '1');
227 }
228 });
229 });
230
231 let $app = $('#formlayer-formbuilder-app');
232 if(!$app.length) return;
233
234 // State management
235 let state = {
236 view: (formlayer_admin.form_id && formlayer_admin.form_id !== '0') ? 'builder' : 'templates',
237 template_category: 'all',
238 template_search: '',
239 field_search: '',
240 display_id: null,
241 form_title: 'Untitled Form',
242 form_settings: {
243 notifications: {
244 enabled: true,
245 to_email: '{admin_email}',
246 reply_to: '',
247 from_name: 'FormLayer',
248 from_email: '{admin_email}',
249 bcc: '',
250 subject: 'New Form Submission',
251 message: 'You have a new submission:\n\n{all_fields}'
252 },
253 confirmations: {
254 type: 'message',
255 message: 'Thank you for your submission!',
256 redirect_url: '',
257 hide_form: true
258 },
259 integrations: {},
260 custom_css: ''
261 },
262 active_tab: 'editor',
263 sidebar_tab: 'input-fields',
264 fields: [],
265 selected_field_id: null,
266 categories: formlayer_admin.categories || [
267 { id: 'general', label: 'General Fields', open: true },
268 { id: 'advanced', label: 'Advanced Fields', open: false },
269 { id: 'premium', label: 'Premium Fields', open: false }
270 ],
271 field_types: formlayer_admin.fieldTypes || [
272 { type: 'name', label: 'Name Fields', icon: 'dashicons-admin-users', category: 'general' },
273 { type: 'email', label: 'Email', icon: 'dashicons-email', category: 'general' },
274 { type: 'text', label: 'Simple Text', icon: 'dashicons-edit', category: 'general' },
275 { type: 'mask', label: 'Mask Input', icon: 'dashicons-shield', category: 'general' },
276 { type: 'textarea', label: 'Text Area', icon: 'dashicons-editor-alignleft', category: 'general' },
277 { type: 'address', label: 'Address Fields', icon: 'dashicons-location', category: 'general' },
278 { type: 'country', label: 'Country List', icon: 'dashicons-flag', category: 'general' },
279 { type: 'number', label: 'Numeric Field', icon: 'dashicons-editor-ol', category: 'general' },
280 { type: 'dropdown', label: 'Dropdown', icon: 'dashicons-arrow-down-alt2', category: 'general' },
281 { type: 'radio', label: 'Radio Field', icon: 'dashicons-marker', category: 'general' },
282 { type: 'checkbox', label: 'Checkbox', icon: 'dashicons-yes', category: 'general' },
283 { type: 'multiple', label: 'Multiple Choice', icon: 'dashicons-list-view', category: 'general' },
284 { type: 'url', label: 'Website URL', icon: 'dashicons-admin-site', category: 'general' },
285 { type: 'date', label: 'Time & Date', icon: 'dashicons-calendar-alt', category: 'general' },
286 { type: 'section', label: 'Section Break', icon: 'dashicons-minus', category: 'general' },
287 { type: 'file', label: 'File Upload', icon: 'dashicons-upload', category: 'premium' },
288 { type: 'image', label: 'Image Upload', icon: 'dashicons-format-image', category: 'premium' },
289 { type: 'phone', label: 'Phone Number', icon: 'dashicons-phone', category: 'premium' },
290 { type: 'rating', label: 'Ratings', icon: 'dashicons-star-filled', category: 'advanced' },
291 { type: 'hidden', label: 'Hidden Field', icon: 'dashicons-visibility-faint', category: 'advanced' },
292 { type: 'password', label: 'Password', icon: 'dashicons-lock', category: 'advanced' },
293 { type: 'captcha', label: 'Captcha (Spam Protection)', icon: 'dashicons-shield', category: 'advanced' },
294 { type: 'terms', label: 'Terms & Conditions', icon: 'dashicons-media-text', category: 'advanced' },
295 { type: 'gdpr', label: 'GDPR Agreement', icon: 'dashicons-shield', category: 'advanced' },
296 { type: 'submit', label: 'Submit Button', icon: 'dashicons-plus-alt', category: 'advanced' }
297 ],
298 templates: formlayer_admin.templates || []
299 };
300
301 // Template engine helper
302 function generate_unique_name_attr(base_name, ignore_id = null) {
303 let slug = (base_name || '').toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
304 if (!slug) slug = 'field';
305
306 let is_unique = false;
307 let name_attr = slug;
308 let counter = 1;
309
310 while (!is_unique) {
311 let exists = state.fields.some(function(f) {
312 return f.id !== ignore_id && (f.name_attr === name_attr || (!f.name_attr && ('field_' + f.id) === name_attr));
313 });
314 if (!exists) {
315 is_unique = true;
316 } else {
317 name_attr = slug + '_' + counter;
318 counter++;
319 }
320 }
321 return name_attr;
322 }
323
324 const utils = {
325 tmpl: function(key, data = {}) {
326 let html = formlayer_admin.html_templates[key] || '';
327 Object.keys(data).forEach(k => {
328 const val = data[k] === undefined || data[k] === null ? '' : data[k];
329 html = html.replace(new RegExp(`{{${k}}}`, 'g'), val);
330 });
331 return html;
332 }
333 };
334
335
336 function load_form(id){
337 $.ajax({
338 url: formlayer_admin.ajax_url,
339 method: 'GET',
340 data: {
341 action: 'formlayer_load_form',
342 nonce: formlayer_admin.nonce,
343 form_id: id
344 },
345 success: function(response){
346 if(typeof response === 'string'){
347 try{ response = JSON.parse(response); } catch(e){}
348 }
349 if(response.success && response.data.form_data){
350 const data = response.data.form_data;
351 state.form_title = data.title || 'Untitled Form';
352 state.fields = data.fields || [];
353 state.form_settings = data.settings || state.form_settings;
354 state.display_id = response.data.display_id;
355 }else if(!response.success){
356 console.error('Form load error:', response);
357 }
358 render();
359 },
360 error: function(jqXHR, textStatus, errorThrown){
361 console.error("AJAX Error details:", textStatus, errorThrown, jqXHR.responseText);
362 if(textStatus !== 'parsererror'){
363 show_toast('Failed to load form. ' + textStatus, 'error');
364 }
365 render();
366 }
367 });
368 }
369
370 function render(){
371 let current_hash = window.location.hash.replace('#', ''),
372 current_tab = current_hash.split('/')[0],
373 is_editing = (formlayer_admin.form_id && formlayer_admin.form_id !== '0'),
374 should_update_hash = (current_tab === 'formbuilder' || (is_editing && !current_tab));
375
376 if(state.view === 'templates'){
377 if(should_update_hash){
378 window.location.hash = 'formbuilder';
379 }
380 $('#formlayer-builder-view').hide();
381 $('#formlayer-templates-view').show();
382 render_templates();
383 }else{
384 if(should_update_hash){
385 window.location.hash = 'formbuilder/customize';
386 }
387 $('#formlayer-templates-view').hide();
388 $('#formlayer-builder-view').show();
389 render_builder();
390 }
391 }
392
393 function render_templates(){
394 let categories = [
395 { id: 'all', label: 'All Templates', icon: 'dashicons-admin-page' },
396 { id: 'general', label: 'General', icon: 'dashicons-forms' },
397 { id: 'education', label: 'Education', icon: 'dashicons-welcome-learn-more' },
398 { id: 'marketing', label: 'Marketing', icon: 'dashicons-share' },
399 { id: 'crm', label: 'Sales & CRM', icon: 'dashicons-admin-users' },
400 { id: 'feedback', label: 'User Feedback', icon: 'dashicons-awards' },
401 { id: 'hr', label: 'HR & Talent', icon: 'dashicons-businessman' },
402 { id: 'it', label: 'IT & Technical', icon: 'dashicons-laptop' },
403 { id: 'finance', label: 'Commercial & Finance', icon: 'dashicons-money-alt' }
404 ];
405
406 let search_term = state.template_search.toLowerCase();
407
408 let filtered = state.templates.filter(function(t){
409 let matches_cat = (state.template_category === 'all' || t.cat === state.template_category || t.id === 'scratch');
410 let matches_search = t.title.toLowerCase().includes(search_term) ||
411 t.desc.toLowerCase().includes(search_term);
412 return matches_cat && matches_search;
413 });
414
415 // Update Sidebar with Counts
416 let cats_html = categories.map(function(cat){
417 let count = state.templates.filter(function(t){
418 let is_cat = (cat.id === 'all' ? t.id !== 'scratch' : t.cat === cat.id);
419 let is_search = t.title.toLowerCase().includes(search_term) || t.desc.toLowerCase().includes(search_term);
420 return is_cat && is_search;
421 }).length;
422
423 return `
424 <li class="${state.template_category === cat.id ? 'active' : ''}" data-cat="${cat.id}">
425 <div class="formlayer-template-cat-item">
426 <span><span class="dashicons ${cat.icon}"></span> ${cat.label}</span>
427 </div>
428 </li>
429 `;
430 }).join('');
431 $('#formlayer-template-cats-list').html(cats_html);
432
433 let grid_html = filtered.length > 0 ? filtered.map(function(t){
434 let badge = '';
435 let is_pro = !!t.is_pro;
436 let pro_active = !!formlayer_admin.is_pro;
437
438 if(t.id === 'scratch'){
439 badge = '<div class="template-header-badge" style="background:var(--fl-primary); color:#fff;">Starter</div>';
440 }else if(is_pro && !pro_active){
441 badge = '<div class="template-header-badge" style="background:#f59e0b; color:#fff;">PRO</div>';
442 }else if(!is_pro){
443 badge = `<div class="template-header-badge">${t.cat ? t.cat.charAt(0).toUpperCase() + t.cat.slice(1) : 'General'}</div>`;
444 }
445
446 let button_html = (is_pro && !pro_active)
447 ? `<button class="formlayer-btn-upgrade-pro formlayer-btn-upgrade-pro-el">Upgrade to PRO</button>`
448 : `<button class="formlayer-btn-use-template">Use Template</button>`;
449
450 return utils.tmpl('template_card', {
451 id: t.id,
452 locked_class: is_pro && !pro_active ? 'is-pro-locked' : '',
453 badge_html: badge,
454 icon: t.icon,
455 title: t.title,
456 desc: t.desc,
457 button_html: button_html
458 });
459 }).join('') : utils.tmpl('no_templates_found');
460 $('#formlayer-templates-grid').html(grid_html);
461 }
462
463 function render_builder(){
464 let shortcode = state.display_id ? `[formlayer id="${state.display_id}"]` : (formlayer_admin.form_id && formlayer_admin.form_id !== '0' ? `[formlayer id="${formlayer_admin.form_id}"]` : 'Save First....');
465
466 $('#formlayer-builder-title').text(state.form_title);
467 $('#formlayer-shortcode-val').val(shortcode);
468
469 // Render Canvas Fields
470 let canvas_html = state.fields.length === 0 ? render_empty_state() : render_fields();
471 $('#formlayer-dropzone').html(canvas_html);
472
473 // Render Sidebar Panes
474 render_sidebar_content();
475 }
476
477 function render_empty_state(){
478 return utils.tmpl('empty_state');
479 }
480
481 function refresh_canvas(){
482 let canvas_html = state.fields.length === 0 ? render_empty_state() : render_fields();
483 $('#formlayer-dropzone').html(canvas_html);
484 }
485
486 function render_fields(){
487 return state.fields.map(function(field, index){
488 let label_placement = field.label_placement || 'top',
489 label_style = field.style_label_color ? `style="color:${field.style_label_color};"` : '',
490 input_style = field.style_border_radius ? `style="border-radius:${field.style_border_radius}px;"` : '';
491
492 let label_html = (!['submit', 'section', 'terms', 'gdpr'].includes(field.type) && label_placement !== 'hidden') ? utils.tmpl('field_label', {
493 label_style: label_style,
494 label: field.label || '',
495 required_mark: field.required ? '<span class="required">*</span>' : ''
496 }) : '';
497
498 let actions_html = utils.tmpl('field_actions', {
499 move_up_disabled: index === 0 ? 'disabled' : '',
500 move_down_disabled: index === state.fields.length - 1 ? 'disabled' : ''
501 });
502
503 return utils.tmpl('field_instance', {
504 id: field.id,
505 active_class: state.selected_field_id === field.id ? 'active' : '',
506 label_placement: label_placement,
507 container_class: field.container_class || '',
508 label_html: label_html,
509 input_style: input_style,
510 input_html: render_field_input(field),
511 help_html: field.help_text ? `<div class="formlayer-field-help">${field.help_text}</div>` : '',
512 actions_html: actions_html
513 });
514 }).join('');
515 }
516
517 function render_field_input(field){
518 const options = Array.isArray(field.options) ? field.options : (typeof field.options === 'string' ? field.options.split('\n') : ['Option 1', 'Option 2', 'Option 3']);
519 const normalized_options = options.map(opt => {
520 if(typeof opt === 'string') return { label: opt, value: opt, default: false };
521 return opt;
522 });
523
524 switch(field.type){
525 case 'textarea': {
526 const rows = field.rows || 3;
527 const cols = field.cols || '';
528 const t_style = cols ? `width:${cols}px !important;` : 'width:100%;';
529 return `<textarea placeholder="${field.placeholder || ''}" rows="${rows}" cols="${cols}" style="${t_style}" disabled>${field.default_value || field.value || ''}</textarea>`;
530 }
531 case 'dropdown':
532 return `
533 <select class="formlayer-builder-select" disabled style="width:100%;">
534 <option value="">${field.placeholder || 'Select Option'}</option>
535 ${normalized_options.map(function(opt){
536 const is_selected = (field.default_value === opt.value || opt.default);
537 return `<option ${is_selected ? 'selected' : ''}>${opt.label}</option>`;
538 }).join('')}
539 </select>`;
540 case 'radio':
541 return `
542 <div class="formlayer-options-preview">
543 ${normalized_options.map(function(opt, i){
544 const is_checked = (field.default_value === opt.value || opt.default);
545 return `
546 <label class="formlayer-option-row">
547 <input type="radio" name="preview_${field.id}" ${is_checked ? 'checked' : ''} disabled> <span>${opt.label}</span>
548 </label>
549 `; }).join('')}
550 </div>`;
551 case 'checkbox':
552 case 'multiple':
553 return `
554 <div class="formlayer-options-preview">
555 ${normalized_options.map(function(opt, i){
556 const is_checked = (Array.isArray(field.default_value) ? field.default_value.includes(opt.value) : (field.default_value === opt.value || opt.default));
557 return `
558 <label class="formlayer-option-row">
559 <input type="checkbox" ${is_checked ? 'checked' : ''} disabled> <span>${opt.label}</span>
560 </label>
561 `; }).join('')}
562 </div>`;
563 case 'hidden':
564 return utils.tmpl('hidden_field_preview', { value: field.value || '(No Value)' });
565 case 'password':
566 return `<input type="password" placeholder="${field.placeholder || '********'}" disabled>`;
567 case 'terms':
568 return `
569 <div class="formlayer-terms-wrap">
570 <input type="checkbox" disabled>
571 <span class="formlayer-terms-label">${field.terms_label || 'I agree to the <a href="#">Terms & Conditions</a>'}</span>
572 </div>`;
573 case 'gdpr':
574 return `
575 <div class="formlayer-gdpr-wrap">
576 <input type="checkbox" disabled>
577 <div style="line-height:1.4;">
578 <div class="formlayer-gdpr-label" style="font-size:14px; color:#475569;">${field.gdpr_label || 'Accept GDPR Policy'}</div>
579 ${field.gdpr_description ? `<div style="color:#64748b; font-size:12px; margin-top:4px;">${field.gdpr_description}</div>` : ''}
580 </div>
581 </div>`;
582 case 'captcha': {
583 const isPro = formlayer_admin && formlayer_admin.is_pro;
584 const prov_label = {
585 hcaptcha: 'hCaptcha',
586 turnstile: isPro ? 'Cloudflare Turnstile' : 'Turnstile (Pro)',
587 recaptcha: isPro ? 'Google reCAPTCHA' : 'reCAPTCHA (Pro)'
588 }[field.captcha_provider || 'hcaptcha'];
589 const theme_label = field.captcha_theme ? field.captcha_theme.charAt(0).toUpperCase() + field.captcha_theme.slice(1) : 'Global Default';
590 return utils.tmpl('captcha_preview', { provider: prov_label + ' (' + theme_label + ')' });
591 }
592 case 'image':
593 case 'file':
594 case 'camera': {
595 const btn_style = `background: ${field.file_btn_bg || '#5525d6'} !important; color: ${field.file_btn_color || '#ffffff'} !important;`;
596 return utils.tmpl('file_upload_box', {
597 btn_style: btn_style,
598 btn_text: 'Choose File',
599 chosen_text: 'No file chosen'
600 });
601 }
602 case 'richtext':
603 return utils.tmpl('richtext_preview');
604 case 'name': {
605 let sub_fields = '';
606 if(field.enable_first_name !== false) sub_fields += utils.tmpl('sub_field_preview', { type:'text', placeholder: field.placeholder_first || 'First Name', label: field.label_first || 'First Name' });
607 if(field.enable_middle_name) sub_fields += utils.tmpl('sub_field_preview', { type:'text', placeholder: field.placeholder_middle || 'Middle Name', label: field.label_middle || 'Middle Name' });
608 if(field.enable_last_name !== false) sub_fields += utils.tmpl('sub_field_preview', { type:'text', placeholder: field.placeholder_last || 'Last Name', label: field.label_last || 'Last Name' });
609 return utils.tmpl('name_fields_preview', { sub_fields: sub_fields });
610 }
611 case 'address': {
612 let sub_fields = '';
613 if(field.enable_street !== false) sub_fields += utils.tmpl('sub_field_preview', { full_width_class:'full-width', type:'text', placeholder: field.placeholder_street || 'Street Address', label: field.label_street || 'Street Address' });
614 if(field.enable_city !== false) sub_fields += utils.tmpl('sub_field_preview', { type:'text', placeholder: field.placeholder_city || 'City', label: field.label_city || 'City' });
615 if(field.enable_state !== false) sub_fields += utils.tmpl('sub_field_preview', { type:'text', placeholder: field.placeholder_state || 'State / Province', label: field.label_state || 'State / Province' });
616 if(field.enable_zip !== false) sub_fields += utils.tmpl('sub_field_preview', { type:'text', placeholder: field.placeholder_zip || 'Zip / Postal Code', label: field.label_zip || 'Zip / Postal Code' });
617 if(field.enable_country !== false) {
618 sub_fields += `
619 <div class="formlayer-sub-field">
620 <select class="formlayer-builder-select" disabled style="width:100%;">
621 <option>${field.placeholder_country || 'Select Country'}</option>
622 <option>United States</option>
623 <option>United Kingdom</option>
624 <option>Canada</option>
625 </select>
626 <div class="formlayer-sub-label">${field.label_country || 'Country'}</div>
627 </div>`;
628 }
629 return utils.tmpl('address_grid_preview', { sub_fields: sub_fields });
630 }
631 case 'number':
632 return `<input type="number" placeholder="${field.placeholder || ''}" min="${field.min || ''}" max="${field.max || ''}" disabled>`;
633 case 'phone':
634 return `
635 <div class="formlayer-input-icon-wrap" style="position:relative;">
636 <span class="dashicons dashicons-phone" style="position:absolute; left:12px; top:50%; transform:translateY(-50%); color:#94a3b8; font-size:18px;"></span>
637 <input type="tel" placeholder="${field.placeholder || 'Phone Number'}" style="padding-left:40px !important;" disabled>
638 </div>`;
639 case 'mask':
640 return `<input type="text" placeholder="${field.placeholder || '(+1) 000-0000'}" disabled>`;
641 case 'country':
642 return `
643 <div class="formlayer-select-wrap">
644 <span class="dashicons dashicons-admin-site"></span>
645 <select class="formlayer-builder-select" disabled style="width:100%;">
646 <option>${field.placeholder || 'Select Country'}</option>
647 <option ${field.default_value === 'US' || field.default_value === 'United States' ? 'selected' : ''}>United States</option>
648 <option ${field.default_value === 'GB' || field.default_value === 'United Kingdom' ? 'selected' : ''}>United Kingdom</option>
649 <option ${field.default_value === 'CA' || field.default_value === 'Canada' ? 'selected' : ''}>Canada</option>
650 <option ${field.default_value === 'AU' || field.default_value === 'Australia' ? 'selected' : ''}>Australia</option>
651 <option ${field.default_value === 'DE' || field.default_value === 'Germany' ? 'selected' : ''}>Germany</option>
652 <option ${field.default_value === 'FR' || field.default_value === 'France' ? 'selected' : ''}>France</option>
653 </select>
654 </div>`;
655 case 'date':
656 return `
657 <div class="formlayer-date-wrap">
658 <input type="date" disabled>
659 <span class="dashicons dashicons-calendar-alt"></span>
660 </div>`;
661 case 'rating':
662 return `
663 <div class="formlayer-rating-preview">
664 ${[1,2,3,4,5].map(function(i){ return `<span class="dashicons dashicons-star-filled" style="color:#e2e8f0; font-size:24px; width:24px; height:24px;"></span>`; }).join('')}
665 </div>`;
666 case 'submit': {
667 let align = field.btn_align || 'left',
668 size = field.btn_size || 'md',
669 bg = field.btn_bg_color || '',
670 txt = field.btn_text_color || '',
671 bg_h = field.btn_bg_hover || '',
672 txt_h = field.btn_text_hover || '',
673 rad = field.style_border_radius ? field.style_border_radius + 'px' : '';
674
675 let b_style = '';
676 if(bg) b_style += `background:${bg} !important;`;
677 if(txt) b_style += `color:${txt} !important;`;
678 if(rad) b_style += `border-radius:${rad} !important;`;
679
680 let btn_id = `btn-preview-${field.id}`;
681 let hover_style = '';
682 if(bg_h || txt_h) {
683 hover_style = `<style>#${btn_id}:hover {
684 ${bg_h ? `background: ${bg_h} !important;` : ''}
685 ${txt_h ? `color: ${txt_h} !important;` : ''}
686 }</style>`;
687 }
688
689 let size_classes = { sm: 'btn-sm', md: 'btn-md', lg: 'btn-lg' },
690 align_classes = { left: 'align-left', center: 'align-center', right: 'align-right', full: 'align-full' };
691
692 return `${hover_style}
693 <div class="formlayer-submit-preview ${align_classes[align]}">
694 <button id="${btn_id}" class="formlayer-submit-btn ${size_classes[size]}" type="button" style="${b_style}">${field.label || 'Submit'}</button>
695 </div>`;
696 }
697 case 'section':
698 return `
699 <div style="border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 15px;">
700 <h3 style="margin:0; font-size:18px; color:#1e293b;">${field.label || 'Section Title'}</h3>
701 ${field.help_text ? `<p style="margin:5px 0 0 0; font-size:13px; color:#64748b;">${field.help_text}</p>` : ''}
702 </div>`;
703 default:
704 return `<input type="text" placeholder="${field.placeholder || ''}" value="${field.default_value || field.value || ''}" disabled>`;
705 }
706 }
707
708 function render_sidebar_content(){
709 $('.formlayer-sidebar-tab').removeClass('active');
710 $(`.formlayer-sidebar-tab[data-tab="${state.sidebar_tab}"]`).addClass('active');
711
712 $('.formlayer-sidebar-pane').removeClass('active');
713 if(state.sidebar_tab === 'input-fields'){
714 $('#formlayer-pane-fields').addClass('active');
715 render_fields_palette();
716 }else{
717 $('#formlayer-pane-customization').addClass('active');
718 render_advanced_options();
719 }
720 }
721
722 function render_fields_palette(){
723 let search_term = (state.field_search || '').toLowerCase();
724
725 let html = state.categories.map(function(cat){
726 let fields = state.field_types.filter(function(f){
727 let matches_cat = f.category === cat.id;
728 let matches_search = !search_term || f.label.toLowerCase().includes(search_term) || f.type.toLowerCase().includes(search_term);
729 return matches_cat && matches_search;
730 });
731
732 if(fields.length === 0 && search_term) return '';
733
734 let fields_html = fields.map(function(f){
735 return utils.tmpl('palette_field', {
736 type: f.type,
737 icon: f.icon || 'dashicons-admin-customizer',
738 label: f.label
739 });
740 }).join('');
741
742 return utils.tmpl('sidebar_category', {
743 open_class: (cat.open || search_term) ? 'open' : '',
744 id: cat.id,
745 label: cat.label,
746 fields_html: fields_html
747 });
748 }).join('');
749
750 let final_html = html || `<div class="formlayer-no-results">No fields found for "${search_term}"</div>`;
751 $('#formlayer-builder-categories').html(final_html);
752 }
753
754 function render_advanced_options(){
755 let field = state.fields.find(function(f){ return f.id === state.selected_field_id; });
756 if(!field){
757 $('#formlayer-pane-customization').html('<div class="formlayer-no-field-selected">Select a field to customize</div>');
758 return;
759 }
760
761 let accordion_open = state.accordion_open || 'general';
762
763 let general_content = '';
764
765 // Admin Label
766 general_content += utils.tmpl('control_group', {
767 label: 'Admin Field Label',
768 info_html: utils.tmpl('info_icon', { title: 'Used only in the admin panel' }),
769 input_html: `<input type="text" class="formlayer-input-full" data-prop="label" value="${field.label || ''}" placeholder="Name">`
770 });
771
772 // Placeholder
773 if(['text', 'email', 'number', 'textarea', 'password', 'url', 'address', 'tel', 'mask', 'country'].includes(field.type)){
774 general_content += utils.tmpl('control_group', {
775 label: 'Placeholder',
776 input_html: `<input type="text" class="formlayer-input-full" data-prop="placeholder" value="${field.placeholder || ''}" placeholder="${field.type === 'country' ? 'Select Country' : 'Placeholder text'}">`
777 });
778 }
779
780 // Label Placement
781 if(!['gdpr', 'submit'].includes(field.type)){
782 general_content += utils.tmpl('control_group', {
783 label: 'Label Placement',
784 info_html: utils.tmpl('info_icon', { title: 'Control where the label appears relative to the input' }),
785 input_html: `
786 <select class="formlayer-input-full" data-prop="label_placement">
787 <option value="top" ${field.label_placement === 'top' ? 'selected' : ''}>Top</option>
788 <option value="left" ${field.label_placement === 'left' ? 'selected' : ''}>Left</option>
789 <option value="right" ${field.label_placement === 'right' ? 'selected' : ''}>Right</option>
790 <option value="hidden" ${field.label_placement === 'hidden' ? 'selected' : ''}>Hidden</option>
791 </select>`
792 });
793 }
794
795 // Options Manager
796 if(['dropdown', 'radio', 'checkbox', 'multiple'].includes(field.type)){
797 const options_html = (field.options || []).map((opt, idx) => {
798 const is_obj = typeof opt === 'object';
799 return utils.tmpl('option_edit_row', {
800 index: idx,
801 default_checked: (is_obj ? opt.default : false) ? 'checked' : '',
802 label: is_obj ? opt.label : opt,
803 value: is_obj ? opt.value : opt
804 });
805 }).join('') + `<button id="formlayer-btn-add-option" class="formlayer-btn-outline" style="width:100%; margin-top:10px;"><span class="dashicons dashicons-plus"></span> Add Option</button>`;
806
807 general_content += utils.tmpl('control_group', {
808 label: 'Field Options',
809 input_html: `<div class="formlayer-options-manager">${options_html}</div>`
810 });
811 }
812
813 // Submit Button specifics
814 if(field.type === 'submit'){
815 general_content += utils.tmpl('control_group', {
816 label: 'Button Alignment',
817 input_html: `
818 <select class="formlayer-input-full" data-prop="btn_align">
819 <option value="left" ${field.btn_align === 'left' ? 'selected' : ''}>Left</option>
820 <option value="center" ${field.btn_align === 'center' ? 'selected' : ''}>Center</option>
821 <option value="right" ${field.btn_align === 'right' ? 'selected' : ''}>Right</option>
822 <option value="full" ${field.btn_align === 'full' ? 'selected' : ''}>Full Width</option>
823 </select>`
824 });
825 general_content += utils.tmpl('control_group', {
826 label: 'Button Size',
827 input_html: `
828 <select class="formlayer-input-full" data-prop="btn_size">
829 <option value="sm" ${field.btn_size === 'sm' ? 'selected' : ''}>Small</option>
830 <option value="md" ${field.btn_size === 'md' ? 'selected' : ''}>Medium</option>
831 <option value="lg" ${field.btn_size === 'lg' ? 'selected' : ''}>Large</option>
832 </select>`
833 });
834 }
835
836 // GDPR specific
837 if(field.type === 'gdpr'){
838 general_content += utils.tmpl('control_group', {
839 label: 'GDPR Label',
840 input_html: `<input type="text" class="formlayer-input-full" data-prop="gdpr_label" value="${field.gdpr_label || 'Accept GDPR Policy'}">`
841 });
842 general_content += utils.tmpl('control_group', {
843 label: 'Policy Description',
844 input_html: `<textarea class="formlayer-input-full" data-prop="gdpr_description" style="height:60px;">${field.gdpr_description || ''}</textarea>`
845 });
846 }
847
848 // Terms specific
849 if(field.type === 'terms'){
850 general_content += utils.tmpl('control_group', {
851 label: 'Terms Label',
852 input_html: `<input type="text" class="formlayer-input-full" data-prop="terms_label" value="${field.terms_label || 'I agree to the <a href=\'#\'>Terms & Conditions</a>'}">`
853 });
854 }
855
856 // Default Value
857 if(!['submit', 'section', 'terms', 'gdpr'].includes(field.type)){
858 general_content += utils.tmpl('control_group', {
859 label: 'Default Value',
860 input_html: `<input type="text" class="formlayer-input-full" data-prop="default_value" value="${field.default_value || field.value || ''}" placeholder="Initial value">`
861 });
862 }
863
864 // Container Class
865 general_content += utils.tmpl('control_group', {
866 label: 'Container CSS Class',
867 input_html: `<input type="text" class="formlayer-input-full" data-prop="container_class" value="${field.container_class || ''}" placeholder="e.g. half-width">`
868 });
869
870 // Help Text
871 general_content += utils.tmpl('control_group', {
872 label: 'Help Message',
873 input_html: `<textarea class="formlayer-input-full" data-prop="help_text" style="height:60px;" placeholder="Brief info for users">${field.help_text || ''}</textarea>`
874 });
875
876 // Style Customization Section
877 let style_html = '<div class="formlayer-divider formlayer-style-divider">Style Customization</div>';
878
879 style_html += `
880 <div class="formlayer-control-group">
881 <div class="formlayer-flex-center-between" style="margin-bottom: 8px;">
882 <label class="formlayer-control-label" style="margin-bottom:0 !important;">Label Color</label>
883 <input type="color" class="formlayer-input-color" data-prop="style_label_color" value="${field.style_label_color || '#334155'}" style="width:30px; height:30px;">
884 </div>
885 ${['file', 'image', 'camera'].includes(field.type) ? `
886 <div class="formlayer-flex-center-between" style="margin-bottom: 8px;">
887 <label class="formlayer-control-label" style="margin-bottom:0 !important;">Button Background</label>
888 <input type="color" class="formlayer-input-color" data-prop="file_btn_bg" value="${field.file_btn_bg || '#5525d6'}" style="width:30px; height:30px;">
889 </div>
890 <div style="display:flex; align-items:center; justify-content:space-between;">
891 <label class="formlayer-control-label" style="margin-bottom:0 !important;">Button Text Color</label>
892 <input type="color" class="formlayer-input-color" data-prop="file_btn_color" value="${field.file_btn_color || '#ffffff'}" style="width:30px; height:30px;">
893 </div>` : ''}
894 </div>`;
895
896 const general_accordion = utils.tmpl('sidebar_accordion', {
897 open_class: accordion_open === 'general' ? 'open' : '',
898 id: 'general',
899 title: (field.label || 'Field') + ' Settings',
900 content_html: general_content + style_html
901 });
902
903 let advanced_content = '';
904
905 // Field Name / Merge Tag
906 advanced_content += utils.tmpl('control_group', {
907 label: 'Name Attribute / Merge Tag',
908 info_html: utils.tmpl('info_icon', { title: 'Use {field_name} in Email Notifications to output this field\'s value' }),
909 input_html: `<input type="text" class="formlayer-input-full" data-prop="name_attr" value="${field.name_attr || generate_unique_name_attr(field.label, field.id)}" placeholder="${generate_unique_name_attr(field.label, field.id)}">`
910 });
911
912 if(field.type === 'name'){
913 advanced_content += `
914 <div class="formlayer-control-group">
915 <label class="formlayer-control-label">Enable Fields</label>
916 <div style="display:grid; grid-template-columns: 1fr; gap:8px;">
917 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_first_name" ${field.enable_first_name !== false ? 'checked' : ''}> First Name</label>
918 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_middle_name" ${field.enable_middle_name ? 'checked' : ''}> Middle Name</label>
919 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_last_name" ${field.enable_last_name !== false ? 'checked' : ''}> Last Name</label>
920 </div>
921 </div>
922 <div class="formlayer-control-group">
923 <label class="formlayer-control-label">Sub Labels</label>
924 <input type="text" class="formlayer-input-full" data-prop="label_first" value="${field.label_first || 'First Name'}" style="margin-bottom:5px;">
925 <input type="text" class="formlayer-input-full" data-prop="label_middle" value="${field.label_middle || 'Middle Name'}" style="margin-bottom:5px;">
926 <input type="text" class="formlayer-input-full" data-prop="label_last" value="${field.label_last || 'Last Name'}">
927 </div>`;
928 }
929
930 if(field.type === 'address'){
931 advanced_content += `
932 <div class="formlayer-control-group">
933 <label class="formlayer-control-label">Enable Fields</label>
934 <div style="display:grid; grid-template-columns: 1fr; gap:8px;">
935 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_street" ${field.enable_street !== false ? 'checked' : ''}> Street Address</label>
936 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_city" ${field.enable_city !== false ? 'checked' : ''}> City</label>
937 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_state" ${field.enable_state !== false ? 'checked' : ''}> State / Province</label>
938 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_zip" ${field.enable_zip !== false ? 'checked' : ''}> Zip / Postal Code</label>
939 <label class="formlayer-flex-center-gap8" style="font-size:13px;"><input type="checkbox" data-prop="enable_country" ${field.enable_country !== false ? 'checked' : ''}> Country List</label>
940 </div>
941 </div>`;
942 }
943
944 if(field.type === 'number'){
945 advanced_content += `
946 <div class="formlayer-control-group" style="display:grid; grid-template-columns: 1fr 1fr; gap:10px;">
947 <div>
948 <label class="formlayer-control-label">Min Value</label>
949 <input type="number" class="formlayer-input-full" data-prop="min" value="${field.min || ''}">
950 </div>
951 <div>
952 <label class="formlayer-control-label">Max Value</label>
953 <input type="number" class="formlayer-input-full" data-prop="max" value="${field.max || ''}">
954 </div>
955 </div>`;
956 }
957
958 if(field.type === 'textarea'){
959 advanced_content += utils.tmpl('control_group', {
960 label: 'Rows (Height)',
961 input_html: `<input type="number" class="formlayer-input-full" data-prop="rows" value="${field.rows || 3}">`
962 });
963 }
964
965 if(field.type === 'date'){
966 advanced_content += utils.tmpl('control_group', {
967 label: 'Date Format',
968 input_html: `
969 <select class="formlayer-input-full" data-prop="date_format">
970 <option value="Y-m-d" ${field.date_format === 'Y-m-d' ? 'selected' : ''}>YYYY-MM-DD</option>
971 <option value="d/m/Y" ${field.date_format === 'd/m/Y' ? 'selected' : ''}>DD/MM/YYYY</option>
972 <option value="m/d/Y" ${field.date_format === 'm/d/Y' ? 'selected' : ''}>MM/DD/YYYY</option>
973 </select>`
974 });
975 }
976
977 if(field.type === 'captcha'){
978 const isPro = formlayer_admin && formlayer_admin.is_pro;
979 advanced_content += utils.tmpl('control_group', {
980 label: 'Provider',
981 input_html: `
982 <select class="formlayer-input-full" data-prop="captcha_provider">
983 <option value="hcaptcha" ${field.captcha_provider === 'hcaptcha' ? 'selected' : ''}>hCaptcha</option>
984 <option value="turnstile" ${!isPro ? 'disabled' : ''} ${field.captcha_provider === 'turnstile' ? 'selected' : ''}>${!isPro ? 'Turnstile (Pro)' : 'Cloudflare Turnstile'}</option>
985 <option value="recaptcha" ${!isPro ? 'disabled' : ''} ${field.captcha_provider === 'recaptcha' ? 'selected' : ''}>${!isPro ? 'reCAPTCHA (Pro)' : 'reCAPTCHA v2'}</option>
986 </select>`
987 });
988 advanced_content += utils.tmpl('control_group', {
989 label: 'Theme',
990 input_html: `
991 <select class="formlayer-input-full" data-prop="captcha_theme">
992 <option value="light" ${field.captcha_theme === 'light' ? 'selected' : ''}>Light</option>
993 <option value="dark" ${field.captcha_theme === 'dark' ? 'selected' : ''}>Dark</option>
994 </select>`
995 });
996 }
997
998 if(field.type === 'submit'){
999 advanced_content += `
1000 <div class="formlayer-control-group">
1001 <label class="formlayer-control-label">Button Styling</label>
1002 <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px;">
1003 <div>
1004 <label style="font-size:11px; color:#64748b; display:block; margin-bottom:4px;">Background</label>
1005 <input type="color" class="formlayer-input-color" data-prop="btn_bg_color" value="${field.btn_bg_color || '#5525d6'}" style="width:100%; height:32px;">
1006 </div>
1007 <div>
1008 <label style="font-size:11px; color:#64748b; display:block; margin-bottom:4px;">Text</label>
1009 <input type="color" class="formlayer-input-color" data-prop="btn_text_color" value="${field.btn_text_color || '#ffffff'}" style="width:100%; height:32px;">
1010 </div>
1011 <div>
1012 <label style="font-size:11px; color:#64748b; display:block; margin-bottom:4px;">Hover Bg</label>
1013 <input type="color" class="formlayer-input-color" data-prop="btn_bg_hover" value="${field.btn_bg_hover || '#441eb1'}" style="width:100%; height:32px;">
1014 </div>
1015 <div>
1016 <label style="font-size:11px; color:#64748b; display:block; margin-bottom:4px;">Hover Text</label>
1017 <input type="color" class="formlayer-input-color" data-prop="btn_text_hover" value="${field.btn_text_hover || '#ffffff'}" style="width:100%; height:32px;">
1018 </div>
1019 </div>
1020 </div>
1021 <div class="formlayer-control-group">
1022 <label class="formlayer-control-label">Border Radius (px)</label>
1023 <input type="number" class="formlayer-input-full" data-prop="style_border_radius" value="${field.style_border_radius || '10'}" min="0" max="50">
1024 </div>`;
1025 }
1026
1027 advanced_content += `
1028 <div class="formlayer-control-group">
1029 <label class="formlayer-control-label" style="display:flex; align-items:center; gap:8px;">
1030 <input type="checkbox" data-prop="required" ${field.required ? 'checked' : ''}>
1031 Required Field
1032 </label>
1033 </div>`;
1034
1035 const advanced_accordion = utils.tmpl('sidebar_accordion', {
1036 open_class: accordion_open === 'advanced' ? 'open' : '',
1037 id: 'advanced',
1038 title: 'Advanced',
1039 content_html: advanced_content
1040 });
1041
1042 $('#formlayer-pane-customization').html(`<div class="formlayer-customization-form">${general_accordion}${advanced_accordion}</div>`);
1043 }
1044
1045 // Helper functions for Form Settings & Entries
1046 function sync_settings_to_ui(){
1047 const s = state.form_settings;
1048 $('#form-setting-notif-enabled').prop('checked', s.notifications.enabled);
1049 $('#form-setting-notif-email').val(s.notifications.to_email);
1050 $('#form-setting-notif-replyto').val(s.notifications.reply_to || '');
1051 $('#form-setting-notif-fromname').val(s.notifications.from_name || '');
1052 $('#form-setting-notif-fromemail').val(s.notifications.from_email || '');
1053 $('#form-setting-notif-bcc').val(s.notifications.bcc || '');
1054 $('#form-setting-notif-subject').val(s.notifications.subject);
1055 $('#form-setting-notif-format').val(s.notifications.format || 'html').trigger('change');
1056 $('#form-setting-notif-message').val(s.notifications.message);
1057 $('#form-setting-conf-type').val(s.confirmations.type).trigger('change');
1058 $('#form-setting-conf-message').val(s.confirmations.message);
1059 $('#form-setting-conf-url').val(s.confirmations.redirect_url);
1060 $('#form-setting-conf-hide').prop('checked', s.confirmations.hide_form !== false);
1061 if(s.integrations){
1062 const ints = ['slack', 'mailchimp', 'sheets', 'notion', 'trello', 'discord'];
1063 ints.forEach(function(slug){
1064 const data = s.integrations[slug] || {};
1065 $(`#form-setting-int-${slug}-enabled`).prop('checked', !!data.enabled).trigger('change');
1066 if(slug === 'slack' || slug === 'discord') $(`#form-setting-int-${slug}-webhook`).val(data.webhook || '');
1067 if(slug === 'mailchimp') $(`#form-setting-int-mailchimp-list`).val(data.list_id || '');
1068 if(slug === 'sheets') {
1069 $(`#form-setting-int-sheets-id`).val(data.spreadsheet_id || '');
1070 $(`#form-setting-int-sheets-name`).val(data.sheet_name || '');
1071 }
1072 if(slug === 'notion') $(`#form-setting-int-notion-db`).val(data.database_id || '');
1073 if(slug === 'trello') $(`#form-setting-int-trello-list`).val(data.list_id || '');
1074 });
1075 }
1076 $('#form-setting-custom-css').val(s.custom_css);
1077
1078 // Render Merge Tags
1079 let tags_html = `<span class="formlayer-badge-tag" data-tag="{all_fields}">{all_fields}</span>`;
1080 tags_html += `<span class="formlayer-badge-tag" data-tag="{admin_email}">{admin_email}</span>`;
1081 tags_html += `<span class="formlayer-badge-tag" data-tag="{form_title}">{form_title}</span>`;
1082 tags_html += `<span class="formlayer-badge-tag" data-tag="{site_url}">{site_url}</span>`;
1083
1084 state.fields.forEach(function(f){
1085 if(['submit', 'section', 'gdpr', 'terms'].includes(f.type)) return;
1086 if (!f.name_attr) {
1087 f.name_attr = generate_unique_name_attr(f.label, f.id);
1088 }
1089 let name = f.name_attr;
1090 let label = f.label || name;
1091 tags_html += `<span class="formlayer-badge-tag" data-tag="{${name}}" title="${label}">{${name}}</span>`;
1092 });
1093
1094 $('#formlayer-dynamic-merge-tags').html(tags_html);
1095 }
1096
1097 function sync_ui_to_settings(){
1098 state.form_settings.notifications.enabled = $('#form-setting-notif-enabled').is(':checked');
1099 state.form_settings.notifications.to_email = $('#form-setting-notif-email').val();
1100 state.form_settings.notifications.reply_to = $('#form-setting-notif-replyto').val();
1101 state.form_settings.notifications.from_name = $('#form-setting-notif-fromname').val();
1102 state.form_settings.notifications.from_email = $('#form-setting-notif-fromemail').val();
1103 state.form_settings.notifications.bcc = $('#form-setting-notif-bcc').val();
1104 state.form_settings.notifications.subject = $('#form-setting-notif-subject').val();
1105 state.form_settings.notifications.format = $('#form-setting-notif-format').val();
1106 state.form_settings.notifications.message = $('#form-setting-notif-message').val();
1107 state.form_settings.confirmations.type = $('#form-setting-conf-type').val();
1108 state.form_settings.confirmations.message = $('#form-setting-conf-message').val();
1109 state.form_settings.confirmations.redirect_url = $('#form-setting-conf-url').val();
1110 state.form_settings.confirmations.hide_form = $('#form-setting-conf-hide').is(':checked');
1111 state.form_settings.integrations = state.form_settings.integrations || {};
1112 ['slack', 'mailchimp', 'sheets', 'notion', 'trello', 'discord'].forEach(function(slug){
1113 const data = { enabled: $(`#form-setting-int-${slug}-enabled`).is(':checked') };
1114 if(slug === 'slack' || slug === 'discord') data.webhook = $(`#form-setting-int-${slug}-webhook`).val();
1115 if(slug === 'mailchimp') data.list_id = $(`#form-setting-int-mailchimp-list`).val();
1116 if(slug === 'sheets') {
1117 data.spreadsheet_id = $(`#form-setting-int-sheets-id`).val();
1118 data.sheet_name = $(`#form-setting-int-sheets-name`).val();
1119 }
1120 if(slug === 'notion') data.database_id = $(`#form-setting-int-notion-db`).val();
1121 if(slug === 'trello') data.list_id = $(`#form-setting-int-trello-list`).val();
1122 state.form_settings.integrations[slug] = data;
1123 });
1124 state.form_settings.custom_css = $('#form-setting-custom-css').val();
1125 }
1126
1127 function save_form(){
1128 const $btn = $('.formlayer-btn-save');
1129 const original_text = $btn.text();
1130
1131 if(state.active_tab === 'settings'){
1132 sync_ui_to_settings();
1133 }
1134
1135 $btn.prop('disabled', true).text('Saving...');
1136
1137 const data = {
1138 action: 'formlayer_save_form',
1139 nonce: formlayer_admin.nonce,
1140 form_id: formlayer_admin.form_id,
1141 title: state.form_title,
1142 fields: JSON.stringify(state.fields),
1143 settings: JSON.stringify(state.form_settings)
1144 };
1145
1146 $.post(formlayer_admin.ajax_url, data, function(response){
1147 if(response.success){
1148 let is_new = (!formlayer_admin.form_id || formlayer_admin.form_id === '0');
1149 if(is_new){
1150 formlayer_admin.form_id = response.data.form_id;
1151 state.display_id = response.data.display_id;
1152 }
1153 show_toast('Form saved successfully!', 'success');
1154
1155 // Dynamically add or update row in forms table without reload
1156 if(response.data.row_html){
1157 let $tbody = $('#formlayer-tab-forms .formlayer-table tbody');
1158 if(is_new){
1159 // Remove empty state row if it exists
1160 if($tbody.find('.formlayer-empty-title').length){
1161 $tbody.empty();
1162 }
1163 // Prepend the new form row
1164 $tbody.prepend(response.data.row_html);
1165 }else{
1166 // Replace existing row
1167 let $existing_row = $tbody.find('.formlayer-row-cb[value="' + response.data.display_id + '"]').closest('tr');
1168 if($existing_row.length){
1169 $existing_row.replaceWith(response.data.row_html);
1170 }
1171 }
1172 }
1173
1174 render();
1175 }else{
1176 show_toast(response.data.message || 'Error saving form', 'error');
1177 }
1178 $btn.prop('disabled', false).text(original_text);
1179 });
1180 }
1181
1182 // Template Search
1183 $('#formlayer-template-search').on('input', function(){
1184 state.template_search = $(this).val();
1185 render_templates();
1186 });
1187
1188 // Template Category Switch
1189 $('#formlayer-template-cats-list').on('click', 'li', function(){
1190 state.template_category = $(this).data('cat');
1191 render_templates();
1192 });
1193
1194 // Field Search
1195 $('.formlayer-fields-search').on('input', function(){
1196 state.field_search = $(this).val();
1197 render_fields_palette();
1198 });
1199
1200 // Field Search Keyboard Shortcut (/)
1201 $(document).on('keydown', function(e){
1202 if(e.key === '/' && !$(e.target).is('input, textarea, select')){
1203 e.preventDefault();
1204 $('.formlayer-fields-search').focus();
1205 }
1206 });
1207
1208 // Template Card Click
1209 $('#formlayer-templates-grid').on('click', '.formlayer-template-card', function(){
1210 if($(this).hasClass('is-pro-locked')) return;
1211 const id = $(this).data('id');
1212 const t = state.templates.find(function(tmp){ return tmp.id === id; });
1213 if(t){
1214 state.form_title = t.title === 'Start from Scratch' ? 'Untitled Form' : t.title;
1215 state.fields = t.fields || [];
1216 state.form_settings = t.settings || state.form_settings;
1217 state.view = 'builder';
1218 render();
1219 }
1220 });
1221
1222 // Back to Templates
1223 $('.formlayer-btn-back-templates').on('click', function(){
1224 if(confirm('Are you sure you want to go back? Unsaved changes will be lost.')){
1225 state.view = 'templates';
1226 state.fields = [];
1227 formlayer_admin.form_id = 0;
1228 render();
1229 }
1230 });
1231
1232 // Sidebar Tab
1233 $('.formlayer-sidebar-tab').on('click', function(){
1234 state.sidebar_tab = $(this).data('tab');
1235 render_sidebar_content();
1236 });
1237
1238 // Category Header Toggle
1239 $('#formlayer-builder-categories').on('click', '.formlayer-category-header', function(){
1240 const $cat = $(this).closest('.formlayer-category');
1241 const cat_id = $cat.data('cat');
1242 const cat = state.categories.find(function(c){ return c.id === cat_id; });
1243 if(cat){
1244 cat.open = !cat.open;
1245 $cat.toggleClass('open');
1246 }
1247 });
1248
1249 // Palette Field Click
1250 $('#formlayer-builder-categories').on('click', '.formlayer-palette-field', function(){
1251 const type = $(this).data('type');
1252 const field_type = state.field_types.find(function(f){ return f.type === type; });
1253 if(field_type){
1254 const new_field = {
1255 id: 'f' + Date.now(),
1256 type: type,
1257 label: field_type.label,
1258 required: false,
1259 options: ['Option 1', 'Option 2', 'Option 3']
1260 };
1261 new_field.name_attr = generate_unique_name_attr(new_field.label, new_field.id);
1262 state.fields.push(new_field);
1263 state.selected_field_id = new_field.id;
1264 render();
1265 }
1266 });
1267
1268 // Field Instance Click
1269 $('#formlayer-dropzone').on('click', '.formlayer-field-instance', function(e){
1270 if($(e.target).closest('.formlayer-field-actions').length) return;
1271 state.selected_field_id = $(this).data('id');
1272 render();
1273 });
1274
1275 // Field Delete
1276 $('#formlayer-dropzone').on('click', '.formlayer-field-delete', function(){
1277 const id = $(this).closest('.formlayer-field-instance').data('id');
1278 state.fields = state.fields.filter(function(f){ return f.id !== id; });
1279 if(state.selected_field_id === id) state.selected_field_id = null;
1280 render();
1281 });
1282
1283 // Field Clone
1284 $('#formlayer-dropzone').on('click', '.formlayer-field-clone', function(){
1285 const id = $(this).closest('.formlayer-field-instance').data('id');
1286 const idx = state.fields.findIndex(function(f){ return f.id === id; });
1287 if(idx !== -1){
1288 const cloned = JSON.parse(JSON.stringify(state.fields[idx]));
1289 cloned.id = 'f' + Date.now();
1290 cloned.name_attr = generate_unique_name_attr(cloned.label, cloned.id);
1291 state.fields.splice(idx + 1, 0, cloned);
1292 render();
1293 }
1294 });
1295
1296 // Field Move Up
1297 $('#formlayer-dropzone').on('click', '.formlayer-field-move-up', function(){
1298 const id = $(this).closest('.formlayer-field-instance').data('id');
1299 const idx = state.fields.findIndex(function(f){ return f.id === id; });
1300 if(idx > 0){
1301 const temp = state.fields[idx];
1302 state.fields[idx] = state.fields[idx - 1];
1303 state.fields[idx - 1] = temp;
1304 render();
1305 }
1306 });
1307
1308 // Field Move Down
1309 $('#formlayer-dropzone').on('click', '.formlayer-field-move-down', function(){
1310 const id = $(this).closest('.formlayer-field-instance').data('id');
1311 const idx = state.fields.findIndex(function(f){ return f.id === id; });
1312 if(idx !== -1 && idx < state.fields.length - 1){
1313 const temp = state.fields[idx];
1314 state.fields[idx] = state.fields[idx + 1];
1315 state.fields[idx + 1] = temp;
1316 render();
1317 }
1318 });
1319
1320 // Customization Form Inputs
1321 $('#formlayer-pane-customization').on('input change', '.formlayer-customization-form input, .formlayer-customization-form select, .formlayer-customization-form textarea', function(){
1322 let prop = $(this).data('prop');
1323 if(!prop) return;
1324 let field = state.fields.find(function(f){ return f.id === state.selected_field_id; });
1325 if(field){
1326 if($(this).attr('type') === 'checkbox'){
1327 field[prop] = $(this).is(':checked');
1328 }else{
1329 field[prop] = $(this).val();
1330 }
1331 refresh_canvas();
1332 }
1333 });
1334
1335 // Accordion Header
1336 $('#formlayer-pane-customization').on('click', '.formlayer-accordion-header', function(){
1337 let $acc = $(this).closest('.formlayer-accordion');
1338 state.accordion_open = $acc.data('accordion');
1339 $('.formlayer-accordion').removeClass('open');
1340 $acc.addClass('open');
1341 });
1342
1343 // Add Option Button
1344 $('#formlayer-pane-customization').on('click', '#formlayer-btn-add-option', function(){
1345 let field = state.fields.find(function(f){ return f.id === state.selected_field_id; });
1346 if(field){
1347 field.options = field.options || [];
1348 field.options.push({ label: 'New Option', value: 'new_option', default: false });
1349 render_advanced_options();
1350 refresh_canvas();
1351 }
1352 });
1353
1354 // Remove Option Button
1355 $('#formlayer-pane-customization').on('click', '.formlayer-btn-remove-option', function(){
1356 let idx = $(this).closest('.formlayer-option-edit-row').data('index'),
1357 field = state.fields.find(function(f){ return f.id === state.selected_field_id; });
1358 if(field && field.options){
1359 field.options.splice(idx, 1);
1360 render_advanced_options();
1361 refresh_canvas();
1362 }
1363 });
1364
1365 // Option Row Inputs
1366 $('#formlayer-pane-customization').on('input change', '.formlayer-option-label, .formlayer-option-value, .formlayer-option-default', function(){
1367 let $row = $(this).closest('.formlayer-option-edit-row'),
1368 idx = $row.data('index'),
1369 field = state.fields.find(function(f){ return f.id === state.selected_field_id; });
1370 if(field && field.options){
1371 let opt = field.options[idx];
1372 if(typeof opt === 'string'){
1373 field.options[idx] = { label: opt, value: opt, default: false };
1374 }
1375
1376 if($(this).hasClass('formlayer-option-label')) field.options[idx].label = $(this).val();
1377 if($(this).hasClass('formlayer-option-value')) field.options[idx].value = $(this).val();
1378 if($(this).hasClass('formlayer-option-default')){
1379 const is_checked = $(this).is(':checked');
1380 if(field.type === 'radio' || field.type === 'dropdown'){
1381 field.options.forEach(o => o.default = false);
1382 field.default_value = is_checked ? field.options[idx].value : '';
1383 }
1384 field.options[idx].default = is_checked;
1385 }
1386 refresh_canvas();
1387 }
1388 });
1389
1390 // Save Button
1391 $('.formlayer-btn-save').on('click', function(){
1392 save_form();
1393 });
1394
1395 // Builder Tab Switch
1396 $('.formlayer-builder-tab').on('click', function(){
1397 state.active_tab = $(this).data('tab');
1398 $('.formlayer-builder-tab').removeClass('active');
1399 $(this).addClass('active');
1400
1401 $('.formlayer-builder-pane').hide();
1402 $(`#formlayer-pane-${state.active_tab}`).show();
1403
1404 if(state.active_tab === 'settings') sync_settings_to_ui();
1405 if(state.active_tab === 'entries' && window.formlayerAdminPro && typeof window.formlayerAdminPro.load_entries === 'function') {
1406 window.formlayerAdminPro.load_entries();
1407 }
1408 });
1409
1410 // Form Settings Modal Open
1411 $('#formlayer-btn-form-settings').on('click', function(){
1412 sync_settings_to_ui();
1413 $('#formlayer-form-settings-modal').addClass('active');
1414 });
1415
1416 // Modal Close
1417 $('.formlayer-modal-close, .formlayer-modal-overlay').on('click', function(e){
1418 if($(e.target).hasClass('formlayer-modal-overlay') || $(e.target).hasClass('formlayer-modal-close')){
1419 $('.formlayer-modal-overlay').removeClass('active');
1420 }
1421 });
1422
1423 // Settings Section Switching
1424 $('.formlayer-modal-sidebar').on('click', 'li', function(){
1425 let section = $(this).data('section');
1426 $('.formlayer-modal-sidebar li').removeClass('active');
1427 $(this).addClass('active');
1428
1429 $('.formlayer-settings-section').hide();
1430 $(`#formlayer-settings-${section}`).show();
1431 });
1432
1433 // Apply Settings
1434 $('#formlayer-settings-apply-btn').on('click', function(){
1435 sync_ui_to_settings();
1436 $('#formlayer-form-settings-modal').removeClass('active');
1437 });
1438
1439 // Device Toggles
1440 $('.formlayer-device-btn').on('click', function(){
1441 let device = $(this).data('device');
1442 $('.formlayer-device-btn').removeClass('active');
1443 $(this).addClass('active');
1444
1445 $('.formlayer-canvas-inner').removeClass('desktop tablet mobile').addClass(device);
1446 });
1447
1448 // Fullscreen Toggle
1449 $('.formlayer-fullscreen-toggle').on('click', function(){
1450 $('.formlayer-admin-wrapper').toggleClass('formlayer-builder-fullscreen');
1451 });
1452
1453 // Insert Merge Tag
1454 $('#formlayer-dynamic-merge-tags').on('click', '.formlayer-badge-tag', function(){
1455 let tag = $(this).data('tag');
1456 let $textarea = $('#form-setting-notif-message');
1457 let pos = $textarea.prop('selectionStart');
1458 let val = $textarea.val();
1459 $textarea.val(val.substring(0, pos) + tag + val.substring(pos));
1460 $textarea.prop('selectionStart', pos + tag.length);
1461 $textarea.prop('selectionEnd', pos + tag.length);
1462 $textarea.focus();
1463 // Trigger change to sync state later if needed
1464 $textarea.trigger('input');
1465 });
1466
1467 // Copy Shortcode
1468 $('.formlayer-copy-shortcode').on('click', function(){
1469 let $input = $('#formlayer-shortcode-val');
1470 $input.select();
1471 document.execCommand('copy');
1472
1473 let original_text = $(this).html();
1474 $(this).html('<span class="dashicons dashicons-yes"></span> Copied!');
1475 setTimeout(() => {
1476 $(this).html(original_text);
1477 }, 2000);
1478 });
1479
1480 // Copy Shortcode from List
1481 $('.formlayer-copy-shortcode-list').on('click', function (e){
1482 e.preventDefault();
1483 let shortcode = $(this).data('shortcode');
1484 if(!shortcode) return;
1485
1486 let $temp = $('<input>');
1487 $('body').append($temp);
1488 $temp.val(shortcode).select();
1489 document.execCommand('copy');
1490 $temp.remove();
1491
1492 let $icon = $(this).find('.dashicons');
1493 $icon.removeClass('dashicons-admin-page').addClass('dashicons-yes');
1494 show_toast('Shortcode copied to clipboard!', 'success');
1495
1496 setTimeout(() => {
1497 $icon.removeClass('dashicons-yes').addClass('dashicons-admin-page');
1498 }, 2000);
1499 });
1500
1501 // Edit Title
1502 $('#formlayer-edit-title-btn').on('click', function(){
1503 $('#formlayer-builder-title').hide();
1504 $('#formlayer-builder-title-input').val(state.form_title).show().focus();
1505 $(this).hide();
1506 });
1507
1508 $('#formlayer-builder-title-input').on('blur', function(){
1509 let new_title = $(this).val().trim() || 'Untitled Form';
1510 state.form_title = new_title;
1511 $('#formlayer-builder-title').text(new_title).show();
1512 $(this).hide();
1513 $('#formlayer-edit-title-btn').show();
1514 });
1515
1516 $('#formlayer-builder-title-input').on('keypress', function(e){
1517 if(e.which === 13){
1518 $(this).blur();
1519 }
1520 });
1521
1522 // Confirmation Type Toggle
1523 $('#form-setting-conf-type').on('change', function(){
1524 if($(this).val() === 'message'){
1525 $('#conf-group-message').show();
1526 $('#conf-group-redirect').hide();
1527 }else{
1528 $('#conf-group-message').hide();
1529 $('#conf-group-redirect').show();
1530 }
1531 });
1532
1533 // Integration Toggle
1534 $('input[id^="form-setting-int-"][id$="-enabled"]').on('change', function(){
1535 let slug = $(this).attr('id').replace('form-setting-int-', '').replace('-enabled', '');
1536 if($(this).is(':checked')){
1537 $(`#${slug}-integration-fields`).show();
1538 }else{
1539 $(`#${slug}-integration-fields`).hide();
1540 }
1541 });
1542
1543 // Initialize
1544 if(formlayer_admin.form_id && formlayer_admin.form_id !== '0'){
1545 load_form(formlayer_admin.form_id);
1546 }else{
1547 render();
1548 }
1549
1550 });