PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.23
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.23
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / js / formidable_admin.js

formidable_admin.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.23, at js/formidable_admin.js

11,287 lines 341.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* exported frm_add_logic_row, frm_remove_tag, frm_show_div, frmCheckAll, frmCheckAllLevel */
2 /* eslint-disable jsdoc/require-param, prefer-const, no-redeclare, @wordpress/no-unused-vars-before-return, jsdoc/check-types, jsdoc/check-tag-names, @wordpress/i18n-translator-comments, @wordpress/valid-sprintf, jsdoc/require-returns-description, jsdoc/require-param-type, no-unused-expressions, compat/compat */
3
4 window.FrmFormsConnect = window.FrmFormsConnect || ( function( document, window, $ ) {
5
6 /*global jQuery:false, frm_admin_js, frmGlobal, ajaxurl */
7
8 const el = {
9 messageBox: null,
10 reset: null,
11
12 setElements: function() {
13 el.messageBox = document.querySelector( '.frm_pro_license_msg' );
14 el.reset = document.getElementById( 'frm_reconnect_link' );
15 }
16 };
17
18 /**
19 * Public functions and properties.
20 *
21 * @since 4.03
22 *
23 * @type {Object}
24 */
25 const app = {
26
27 /**
28 * Register connect button event.
29 *
30 * @since 4.03
31 */
32 init: function() {
33 el.setElements();
34
35 $( document.getElementById( 'frm_deauthorize_link' ) ).on( 'click', app.deauthorize );
36 $( '.frm_authorize_link' ).on( 'click', app.authorize );
37 // Handles FF dashboard Authorize & Reauthorize events.
38 // Attach click event to parent as #frm_deauthorize_link & #frm_reconnect_link dynamically recreated by bootstrap.setupBootstrapDropdowns in dom.js
39 $( '.frm-dashboard-license-options' ).on( 'click', '#frm_deauthorize_link', app.deauthorize );
40 $( '.frm-dashboard-license-options' ).on( 'click', '#frm_reconnect_link', app.reauthorize );
41
42 if ( el.reset !== null ) {
43 $( el.reset ).on( 'click', app.reauthorize );
44 }
45 },
46
47 /* Manual license authorization */
48 authorize: function() {
49 /*jshint validthis:true */
50 const button = this;
51 const pluginSlug = this.getAttribute( 'data-plugin' );
52 const input = document.getElementById( 'edd_' + pluginSlug + '_license_key' );
53 const license = input.value;
54 let wpmu = document.getElementById( 'proplug-wpmu' );
55 this.classList.add( 'frm_loading_button' );
56 if ( wpmu === null ) {
57 wpmu = 0;
58 } else if ( wpmu.checked ) {
59 wpmu = 1;
60 } else {
61 wpmu = 0;
62 }
63
64 $.ajax({
65 type: 'POST', url: ajaxurl, dataType: 'json',
66 data: {
67 action: 'frm_addon_activate',
68 license: license,
69 plugin: pluginSlug,
70 wpmu: wpmu,
71 nonce: frmGlobal.nonce
72 },
73 success: function( msg ) {
74 app.afterAuthorize( msg, input );
75 button.classList.remove( 'frm_loading_button' );
76 }
77 });
78 },
79
80 afterAuthorize: function( msg, input ) {
81 if ( msg.success === true ) {
82 input.value = '•••••••••••••••••••';
83 }
84
85 wp.hooks.doAction( 'frm_after_authorize', msg );
86 app.showMessage( msg );
87 },
88
89 showProgress: function( msg ) {
90 if ( el.messageBox === null ) {
91 // In case the message box was added after page load.
92 el.setElements();
93 }
94
95 const messageBox = el.messageBox;
96 if ( messageBox === null ) {
97 return;
98 }
99
100 if ( msg.success === true ) {
101 messageBox.classList.remove( 'frm_error_style' );
102 messageBox.classList.add( 'frm_message', 'frm_updated_message' );
103 } else {
104 messageBox.classList.add( 'frm_error_style' );
105 messageBox.classList.remove( 'frm_message', 'frm_updated_message' );
106 }
107 messageBox.classList.remove( 'frm_hidden' );
108 messageBox.innerHTML = msg.message;
109 },
110
111 showMessage: function( msg ) {
112 if ( el.messageBox === null ) {
113 // In case the message box was added after page load.
114 el.setElements();
115 }
116 const messageBox = el.messageBox;
117
118 if ( msg.success === true ) {
119 app.showAuthorized( true );
120 app.showInlineSuccess();
121
122 /**
123 * Triggers the after license is authorized action for a confirmation/success modal.
124 * @param {Object} msg An object containing message data received from Authorize request.
125 */
126 wp.hooks.doAction( 'frmAdmin.afterLicenseAuthorizeSuccess', { msg });
127 }
128 app.showProgress( msg );
129
130 if ( msg.message !== '' ) {
131 setTimeout( function() {
132 messageBox.innerHTML = '';
133 messageBox.classList.add( 'frm_hidden' );
134 messageBox.classList.remove( 'frm_error_style', 'frm_message', 'frm_updated_message' );
135 }, 10000 );
136 const refreshPage = document.querySelector( '.frm-admin-page-dashboard' );
137 if ( refreshPage ) {
138 setTimeout( function() {
139 window.location.reload();
140 }, 1000 );
141 }
142 }
143 },
144
145 showAuthorized: function( show ) {
146 const from = show ? 'unauthorized' : 'authorized';
147 const to = show ? 'authorized' : 'unauthorized';
148 const container = document.querySelectorAll( '.frm_' + from + '_box' );
149 if ( container.length ) {
150 // Replace all authorized boxes with unauthorized boxes.
151 container.forEach( function( box ) {
152 box.className = box.className.replace( 'frm_' + from + '_box', 'frm_' + to + '_box' );
153 });
154 }
155 },
156
157 /**
158 * Use the data-success element to replace the element content.
159 */
160 showInlineSuccess: function() {
161 const successElement = document.querySelectorAll( '.frm-confirm-msg [data-success]' );
162 if ( successElement.length ) {
163 successElement.forEach( function( element ) {
164 element.innerHTML = frmAdminBuild.purifyHtml( element.getAttribute( 'data-success' ) );
165 });
166 }
167 },
168
169 /* Clear the site license cache */
170 reauthorize: function() {
171 /*jshint validthis:true */
172 this.innerHTML = '<span class="frm-wait frm_spinner" style="visibility:visible;float:none"></span>';
173
174 $.ajax({
175 type: 'POST',
176 url: ajaxurl,
177 dataType: 'json',
178 data: {
179 action: 'frm_reset_cache',
180 plugin: 'formidable_pro',
181 nonce: frmGlobal.nonce
182 },
183 success: function( msg ) {
184 el.reset.textContent = msg.message;
185 if ( el.reset.getAttribute( 'data-refresh' ) === '1' ) {
186 window.location.reload();
187 }
188 }
189 });
190 return false;
191 },
192
193 deauthorize: function() {
194 /*jshint validthis:true */
195 if ( ! confirm( frmGlobal.deauthorize ) ) {
196 return false;
197 }
198 const pluginSlug = this.getAttribute( 'data-plugin' ),
199 input = document.getElementById( 'edd_' + pluginSlug + '_license_key' ),
200 license = input.value,
201 link = this;
202
203 this.innerHTML = '<span class="frm-wait frm_spinner" style="visibility:visible;"></span>';
204
205 $.ajax({
206 type: 'POST',
207 url: ajaxurl,
208 data: {
209 action: 'frm_addon_deactivate',
210 license: license,
211 plugin: pluginSlug,
212 nonce: frmGlobal.nonce
213 },
214 success: function() {
215 app.showAuthorized( false );
216 input.value = '';
217 link.replaceWith( 'Disconnected' );
218
219 /**
220 * Triggers the after license is deauthorized sruccess action.
221 */
222 wp.hooks.doAction( 'frmAdmin.afterLicenseDeauthorizeSuccess', {});
223
224 }
225 });
226 return false;
227 }
228 };
229
230 // Provide access to public functions/properties.
231 return app;
232
233 }( document, window, jQuery ) );
234
235 function frmAdminBuildJS() {
236 //'use strict';
237
238 /*global jQuery:false, frm_admin_js, frmGlobal, ajaxurl, fromDom */
239
240 const frmAdminJs = frm_admin_js; // eslint-disable-line camelcase
241 const { tag, div, span, a, svg, img } = frmDom;
242 const { onClickPreventDefault } = frmDom.util;
243 const { doJsonFetch, doJsonPost } = frmDom.ajax;
244 frmAdminJs.contextualShortcodes = getContextualShortcodes();
245 const icons = {
246 save: svg({ href: '#frm_save_icon' }),
247 drag: svg({ href: '#frm_drag_icon', classList: [ 'frm_drag_icon', 'frm-drag' ] })
248 };
249
250 let $newFields = jQuery( document.getElementById( 'frm-show-fields' ) ),
251 builderForm = document.getElementById( 'new_fields' ),
252 thisForm = document.getElementById( 'form_id' ),
253 copyHelper = false,
254 fieldsUpdated = 0,
255 thisFormId = 0,
256 autoId = 0,
257 optionMap = {},
258 lastNewActionIdReturned = 0;
259
260 const { __, sprintf } = wp.i18n;
261 let debouncedSyncAfterDragAndDrop, postBodyContent, $postBodyContent;
262
263 const dragState = {
264 dragging: false
265 };
266
267 if ( thisForm !== null ) {
268 thisFormId = thisForm.value;
269 }
270
271 const currentURL = new URL( window.location.href );
272 const urlParams = currentURL.searchParams;
273 const builderPage = document.getElementById( 'frm_builder_page' );
274
275 // Global settings
276 let s;
277
278 function showElement( element ) {
279 if ( ! element[0]) {
280 return;
281 }
282 element[0].style.display = '';
283 }
284
285 function empty( $obj ) {
286 if ( $obj !== null ) {
287 while ( $obj.firstChild ) {
288 $obj.removeChild( $obj.firstChild );
289 }
290 }
291 }
292
293 function addClass( $obj, className ) {
294 if ( $obj.classList ) {
295 $obj.classList.add( className );
296 } else {
297 $obj.className += ' ' + className;
298 }
299 }
300
301 function confirmClick( e ) {
302 /*jshint validthis:true */
303 e.stopPropagation();
304 e.preventDefault();
305 confirmLinkClick( this );
306 }
307
308 function confirmLinkClick( link ) {
309 const message = link.getAttribute( 'data-frmverify' ),
310 loadedFrom = link.getAttribute( 'data-loaded-from' ) ;
311
312 if ( message === null || link.id === 'frm-confirmed-click' ) {
313 return true;
314 }
315
316 if ( 'entries-list' === loadedFrom ) {
317 return wp.hooks.applyFilters( 'frm_on_multiple_entries_delete', { link, initModal });
318 }
319
320 return confirmModal( link );
321 }
322
323 function confirmModal( link ) {
324 let verify, $confirmMessage, i, dataAtts, btnClass,
325 $info = initModal( '#frm_confirm_modal', '400px' ),
326 continueButton = document.getElementById( 'frm-confirmed-click' );
327
328 if ( $info === false ) {
329 return false;
330 }
331
332 verify = link.getAttribute( 'data-frmverify' );
333 btnClass = verify ? link.getAttribute( 'data-frmverify-btn' ) : '';
334 $confirmMessage = jQuery( '.frm-confirm-msg' );
335 $confirmMessage.empty();
336
337 if ( verify ) {
338 $confirmMessage.append( document.createTextNode( verify ) );
339 if ( btnClass ) {
340 continueButton.classList.add( btnClass );
341 }
342 }
343
344 removeAtts = continueButton.dataset;
345 for ( i in dataAtts ) {
346 continueButton.removeAttribute( 'data-' + i );
347 }
348
349 dataAtts = link.dataset;
350 for ( i in dataAtts ) {
351 if ( i !== 'frmverify' ) {
352 continueButton.setAttribute( 'data-' + i, dataAtts[i]);
353 }
354 }
355
356 /**
357 * Triggers the pre-open action for a confirmation modal. This action passes
358 * relevant modal information and associated link to any listening hooks.
359 *
360 * @param {Object} options An object containing modal elements and data.
361 * @param {HTMLElement} options.$info The HTML element containing modal information.
362 * @param {string} options.link The link associated with the modal action.
363 */
364 wp.hooks.doAction( 'frmAdmin.beforeOpenConfirmModal', { $info, link });
365
366 $info.dialog( 'open' );
367 continueButton.setAttribute( 'href', link.getAttribute( 'href' ) || link.getAttribute( 'data-href' ) );
368 return false;
369 }
370
371 function infoModal( msg ) {
372 const $info = initModal( '#frm_info_modal', '400px' );
373
374 if ( $info === false ) {
375 return false;
376 }
377
378 jQuery( '.frm-info-msg' ).html( msg );
379
380 $info.dialog( 'open' );
381 return false;
382 }
383
384 function toggleItem( e ) {
385 /*jshint validthis:true */
386 const toggle = this.getAttribute( 'data-frmtoggle' );
387 const text = this.getAttribute( 'data-toggletext' );
388 const $items = jQuery( toggle );
389
390 e.preventDefault();
391
392 $items.toggle();
393
394 if ( text !== null && text !== '' ) {
395 this.setAttribute( 'data-toggletext', this.innerHTML );
396 this.textContent = text;
397 }
398
399 return false;
400 }
401
402 /**
403 * Toggle a class on target elements when an anchor is clicked, or when a radio or checkbox has been selected.
404 *
405 * @param {Event} e Event with either the change or click type.
406 * @returns {false}
407 */
408 function hideShowItem( e ) {
409 /*jshint validthis:true */
410 let hide = this.getAttribute( 'data-frmhide' );
411 let show = this.getAttribute( 'data-frmshow' );
412 let uncheckList = this.getAttribute( 'data-frmuncheck' );
413 let uncheckListArray = uncheckList ? uncheckList.split( ',' ) : [];
414
415 // Flip unchecked checkboxes so an off value undoes the on value.
416 if ( isUncheckedCheckbox( this ) ) {
417 if ( hide !== null ) {
418 show = hide;
419 hide = null;
420 } else if ( show !== null ) {
421 hide = show;
422 show = null;
423 }
424 }
425
426 e.preventDefault();
427
428 const toggleClass = this.getAttribute( 'data-toggleclass' ) || 'frm_hidden';
429
430 if ( hide !== null ) {
431 jQuery( hide ).addClass( toggleClass );
432 }
433
434 if ( show !== null ) {
435 jQuery( show ).removeClass( toggleClass );
436 }
437
438 const current = this.parentNode.querySelectorAll( 'a.current' );
439 if ( current !== null ) {
440 for ( let i = 0; i < current.length; i++ ) {
441 current[ i ].classList.remove( 'current' );
442 }
443 this.classList.add( 'current' );
444 }
445
446 if ( uncheckListArray.length ) {
447 uncheckListArray.forEach( function( uncheckItem ) {
448 const uncheckItemElement = document.querySelector( uncheckItem );
449 if ( uncheckItemElement ) {
450 uncheckItemElement.checked = false;
451 }
452 });
453 }
454
455 return false;
456 }
457
458 function isUncheckedCheckbox( element ) {
459 return 'INPUT' === element.nodeName && 'checkbox' === element.type && ! element.checked;
460 }
461
462 function loadTooltips() {
463 let wrapClass = jQuery( '.wrap, .frm_wrap' ),
464 confirmModal = document.getElementById( 'frm_confirm_modal' ),
465 doAction = false,
466 confirmedBulkDelete = false;
467
468 jQuery( confirmModal ).on( 'click', '[data-deletefield]', deleteFieldConfirmed );
469 jQuery( confirmModal ).on( 'click', '[data-removeid]', removeThisTag );
470 jQuery( confirmModal ).on( 'click', '[data-trashtemplate]', trashTemplate );
471
472 wrapClass.on( 'click', '.frm_remove_tag, .frm_remove_form_action', removeThisTag );
473 wrapClass.on( 'click', 'a[data-frmverify]', confirmClick );
474 wrapClass.on( 'click', 'a[data-frmtoggle]', toggleItem );
475 wrapClass.on( 'click', 'a[data-frmhide], a[data-frmshow]', hideShowItem );
476 wrapClass.on( 'change', 'input[data-frmhide], input[data-frmshow]', hideShowItem );
477 wrapClass.on( 'click', '.widget-top,a.widget-action', clickWidget );
478
479 wrapClass.on( 'mouseenter.frm', '.frm_bstooltip, .frm_help', function() {
480 jQuery( this ).off( 'mouseenter.frm' );
481
482 jQuery( '.frm_bstooltip, .frm_help' ).tooltip();
483 jQuery( this ).tooltip( 'show' );
484 });
485
486 jQuery( '.frm_bstooltip, .frm_help' ).tooltip( );
487
488 jQuery( document ).on( 'click', '#doaction, #doaction2', function( event ) {
489 const isTop = this.id === 'doaction',
490 suffix = isTop ? 'top' : 'bottom',
491 bulkActionSelector = document.getElementById( 'bulk-action-selector-' + suffix ),
492 confirmBulkDelete = document.getElementById( 'confirm-bulk-delete-' + suffix );
493
494 if ( bulkActionSelector !== null && confirmBulkDelete !== null ) {
495 doAction = this;
496
497 if ( ! confirmedBulkDelete && bulkActionSelector.value === 'bulk_delete' ) {
498 event.preventDefault();
499 confirmLinkClick( confirmBulkDelete );
500 return false;
501 }
502 } else {
503 doAction = false;
504 }
505 });
506
507 jQuery( document ).on( 'click', '#frm-confirmed-click', function( event ) {
508 if ( doAction === false || event.target.classList.contains( 'frm-btn-inactive' ) ) {
509 return;
510 }
511
512 if ( this.getAttribute( 'href' ) === 'confirm-bulk-delete' ) {
513 event.preventDefault();
514 confirmedBulkDelete = true;
515 doAction.click();
516 return false;
517 }
518 });
519 }
520
521 function deleteTooltips() {
522 document.querySelectorAll( '.tooltip' ).forEach(
523 function( tooltip ) {
524 tooltip.remove();
525 }
526 );
527 }
528
529 function removeThisTag() {
530 /*jshint validthis:true */
531 let show, hide, removeMore;
532
533 if ( parseInt( this.getAttribute( 'data-skip-frm-js' ) ) || confirmLinkClick( this ) === false ) {
534 return;
535 }
536
537 const deleteButton = jQuery( this );
538 const id = deleteButton.attr( 'data-removeid' );
539
540 show = deleteButton.attr( 'data-showlast' );
541 if ( typeof show === 'undefined' ) {
542 show = '';
543 }
544
545 hide = deleteButton.attr( 'data-hidelast' );
546 if ( typeof hide === 'undefined' ) {
547 hide = '';
548 }
549
550 removeMore = deleteButton.attr( 'data-removemore' );
551
552 if ( show !== '' ) {
553 if ( deleteButton.closest( '.frm_add_remove' ).find( '.frm_remove_tag:visible' ).length > 1 ) {
554 show = '';
555 hide = '';
556 }
557 } else if ( id.indexOf( 'frm_postmeta_' ) === 0 ) {
558 if ( jQuery( '#frm_postmeta_rows .frm_postmeta_row' ).length < 2 ) {
559 show = '.frm_add_postmeta_row.button';
560 }
561 if ( jQuery( '.frm_toggle_cf_opts' ).length && jQuery( '#frm_postmeta_rows .frm_postmeta_row:not(#' + id + ')' ).last().length ) {
562 if ( show !== '' ) {
563 show += ',';
564 }
565 show += '#' + jQuery( '#frm_postmeta_rows .frm_postmeta_row:not(#' + id + ')' ).last().attr( 'id' ) + ' .frm_toggle_cf_opts';
566 }
567 }
568
569 const $fadeEle = jQuery( document.getElementById( id ) );
570 $fadeEle.fadeOut( 400, function() {
571 $fadeEle.remove();
572 fieldUpdated();
573
574 if ( hide !== '' ) {
575 jQuery( hide ).hide();
576 }
577
578 if ( show !== '' ) {
579 jQuery( show + ' a,' + show ).removeClass( 'frm_hidden' ).fadeIn( 'slow' );
580 }
581
582 if ( this.closest( '.frm_form_action_settings' ) ) {
583 const type = this.closest( '.frm_form_action_settings' ).querySelector( '.frm_action_name' ).value;
584 afterActionRemoved( type );
585 }
586 document.querySelector( '.tooltip' )?.remove();
587 });
588
589 if ( typeof removeMore !== 'undefined' ) {
590 removeMore = jQuery( removeMore );
591 removeMore.fadeOut( 400, function() {
592 removeMore.remove();
593 });
594 }
595
596 if ( show !== '' ) {
597 jQuery( this ).closest( '.frm_logic_rows' ).fadeOut( 'slow' );
598 }
599
600 return false;
601 }
602
603 function afterActionRemoved( type ) {
604 checkActiveAction( type );
605
606 const hookName = 'frm_after_action_removed';
607 const hookArgs = { type };
608 wp.hooks.doAction( hookName, hookArgs );
609 }
610
611 function clickWidget( event, b ) {
612 /*jshint validthis:true */
613 if ( typeof b === 'undefined' ) {
614 b = this;
615 }
616
617 popCalcFields( b, false );
618
619 const cont = jQuery( b ).closest( '.frm_form_action_settings' );
620 const target = event.target;
621
622 if ( cont.length && typeof target !== 'undefined' ) {
623 const className = target.parentElement.className;
624 if ( 'string' === typeof className ) {
625 if ( className.indexOf( 'frm_email_icons' ) > -1 || className.indexOf( 'frm_toggle' ) > -1 ) {
626 // clicking on delete icon shouldn't open it
627 event.stopPropagation();
628 return;
629 }
630 }
631 }
632
633 let inside = cont.children( '.widget-inside' );
634
635 if ( cont.length && inside.find( 'p, div, table' ).length < 1 ) {
636 const actionId = cont.find( 'input[name$="[ID]"]' ).val();
637 const actionType = cont.find( 'input[name$="[post_excerpt]"]' ).val();
638 if ( actionType ) {
639 inside.html( '<span class="frm-wait frm_spinner"></span>' );
640 cont.find( '.spinner' ).fadeIn( 'slow' );
641 jQuery.ajax({
642 type: 'POST',
643 url: ajaxurl,
644 data: {
645 action: 'frm_form_action_fill',
646 action_id: actionId,
647 action_type: actionType,
648 nonce: frmGlobal.nonce
649 },
650 success: function( html ) {
651 inside.html( html );
652 initiateMultiselect();
653 showInputIcon( '#' + cont.attr( 'id' ) );
654 initAutocomplete( inside );
655 jQuery( b ).trigger( 'frm-action-loaded' );
656
657 /**
658 * Fires after filling form action content when opening.
659 *
660 * @since 5.5.4
661 *
662 * @param {Object} insideElement JQuery object of form action inside element.
663 */
664 wp.hooks.doAction( 'frm_filled_form_action', inside );
665 }
666 });
667 }
668 }
669
670 jQuery( b ).closest( '.frm_field_box' ).siblings().find( '.widget-inside' ).slideUp( 'fast' );
671 if ( ( typeof b.className !== 'undefined' && b.className.indexOf( 'widget-action' ) !== -1 ) || jQuery( b ).closest( '.start_divider' ).length < 1 ) {
672 return;
673 }
674
675 inside = jQuery( b ).closest( 'div.widget' ).children( '.widget-inside' );
676 if ( inside.is( ':hidden' ) ) {
677 inside.slideDown( 'fast' );
678 } else {
679 inside.slideUp( 'fast' );
680 }
681 }
682
683 function clickNewTab() {
684 /*jshint validthis:true */
685 const t = this.getAttribute( 'href' );
686 if ( typeof t === 'undefined' ) {
687 return false;
688 }
689
690 const c = t.replace( '#', '.' );
691 const $link = jQuery( this );
692
693 $link.closest( 'li' ).addClass( 'frm-tabs active' ).siblings( 'li' ).removeClass( 'frm-tabs active starttab' );
694 $link.closest( 'div' ).children( '.tabs-panel' ).not( t ).not( c ).hide();
695
696 const tabContent = document.getElementById( t.replace( '#', '' ) );
697 if ( tabContent ) {
698 tabContent.style.display = 'block';
699 }
700
701 // clearSettingsBox would hide field settings when opening the fields modal and we want to skip it there.
702 if ( this.id === 'frm_insert_fields_tab' && ! this.closest( '#frm_adv_info' ) ) {
703 clearSettingsBox();
704 }
705 return false;
706 }
707
708 function clickTab( link, auto ) {
709 link = jQuery( link );
710 const t = link.attr( 'href' );
711 if ( typeof t === 'undefined' ) {
712 return;
713 }
714
715 const c = t.replace( '#', '.' );
716
717 link.closest( 'li' ).addClass( 'frm-tabs active' ).siblings( 'li' ).removeClass( 'frm-tabs active starttab' );
718 if ( link.closest( 'div' ).find( '.tabs-panel' ).length ) {
719 link.closest( 'div' ).children( '.tabs-panel' ).not( t ).not( c ).hide();
720 } else if ( document.getElementById( 'form_global_settings' ) !== null ) {
721 /* global settings */
722 const ajax = link.data( 'frmajax' );
723 link.closest( '.frm_wrap' ).find( '.tabs-panel, .hide_with_tabs' ).hide();
724 if ( typeof ajax !== 'undefined' && ajax == '1' ) {
725 loadSettingsTab( t );
726 }
727 } else {
728 /* form settings page */
729 jQuery( '#frm-categorydiv .tabs-panel, .hide_with_tabs' ).hide();
730 }
731 jQuery( t ).show();
732 jQuery( c ).show();
733
734 hideShortcodes();
735
736 if ( auto !== 'auto' ) {
737 // Hide success message on tab change.
738 jQuery( '.frm_updated_message' ).hide();
739 jQuery( '.frm_warning_style' ).hide();
740 }
741
742 if ( jQuery( link ).closest( '#frm_adv_info' ).length ) {
743 return;
744 }
745
746 if ( jQuery( '.frm_form_settings' ).length ) {
747 jQuery( '.frm_form_settings' ).attr( 'action', '?page=formidable&frm_action=settings&id=' + jQuery( '.frm_form_settings input[name="id"]' ).val() + '&t=' + t.replace( '#', '' ) );
748 } else {
749 jQuery( '.frm_settings_form' ).attr( 'action', '?page=formidable-settings&t=' + t.replace( '#', '' ) );
750 }
751 }
752
753 function setupSortable( sortableSelector ) {
754 document.querySelectorAll( sortableSelector ).forEach(
755 list => {
756 makeDroppable( list );
757 Array.from( list.children ).forEach( child => makeDraggable( child, '.frm-move' ) );
758
759 const $sectionTitle = jQuery( list ).children( '[data-type="divider"]' ).children( '.divider_section_only' );
760 if ( $sectionTitle.length ) {
761 makeDroppable( $sectionTitle );
762 }
763 }
764 );
765 setupFieldOptionSorting( jQuery( '#frm_builder_page' ) );
766 }
767
768 function makeDroppable( list ) {
769 jQuery( list ).droppable({
770 accept: '.frmbutton, li.frm_field_box',
771 deactivate: handleFieldDrop,
772 over: onDragOverDroppable,
773 out: onDraggableLeavesDroppable,
774 tolerance: 'pointer'
775 });
776 }
777
778 function onDragOverDroppable( event, ui ) {
779 const droppable = getDroppableForOnDragOver( event.target );
780 const draggable = ui.draggable[0];
781
782 if ( ! allowDrop( draggable, droppable, event ) ) {
783 droppable.classList.remove( 'frm-over-droppable' );
784 jQuery( droppable ).parents( 'ul.frm_sorting' ).addClass( 'frm-over-droppable' );
785 return;
786 }
787
788 document.querySelectorAll( '.frm-over-droppable' ).forEach( droppable => droppable.classList.remove( 'frm-over-droppable' ) );
789 droppable.classList.add( 'frm-over-droppable' );
790 jQuery( droppable ).parents( 'ul.frm_sorting' ).addClass( 'frm-over-droppable' );
791 }
792
793 /**
794 * Maybe change the droppable.
795 * Section titles are made droppable, but are not a list, so we need to change the droppable to the section's list instead.
796 *
797 * @param {Element} droppable
798 * @returns {Element}
799 */
800 function getDroppableForOnDragOver( droppable ) {
801 if ( droppable.classList.contains( 'divider_section_only' ) ) {
802 droppable = jQuery( droppable ).nextAll( '.start_divider.frm_sorting' ).get( 0 );
803 }
804 return droppable;
805 }
806
807 function onDraggableLeavesDroppable( event ) {
808 const droppable = event.target;
809 droppable.classList.remove( 'frm-over-droppable' );
810 }
811
812 function makeDraggable( draggable, handle ) {
813 const settings = {
814 helper: getDraggableHelper,
815 revert: 'invalid',
816 delay: 10,
817 start: handleDragStart,
818 stop: handleDragStop,
819 drag: handleDrag,
820 cursor: 'grabbing',
821 refreshPositions: true,
822 cursorAt: {
823 top: 0,
824 left: 90 // The width of draggable button is 180. 90 should center the draggable on the cursor.
825 }
826 };
827 if ( 'string' === typeof handle ) {
828 settings.handle = handle;
829 }
830 jQuery( draggable ).draggable( settings );
831 }
832
833 function getDraggableHelper( event ) {
834 const draggable = event.delegateTarget;
835
836 if ( isFieldGroup( draggable ) ) {
837 const newTextFieldClone = document.getElementById( 'frm-insert-fields' ).querySelector( '.frm_ttext' ).cloneNode( true );
838 newTextFieldClone.querySelector( 'use' ).setAttributeNS( 'http://www.w3.org/1999/xlink', 'href', '#frm_field_group_layout_icon' );
839 newTextFieldClone.querySelector( 'span' ).textContent = __( 'Field Group', 'formidable' );
840 newTextFieldClone.classList.add( 'frm_field_box' );
841 newTextFieldClone.classList.add( 'ui-sortable-helper' );
842 return newTextFieldClone;
843 }
844
845 let copyTarget;
846 const isNewField = draggable.classList.contains( 'frmbutton' );
847 if ( isNewField ) {
848 copyTarget = draggable.cloneNode( true );
849 copyTarget.classList.add( 'ui-sortable-helper' );
850 draggable.classList.add( 'frm-new-field' );
851 return copyTarget;
852 }
853
854 if ( draggable.hasAttribute( 'data-ftype' ) ) {
855 const fieldType = draggable.getAttribute( 'data-ftype' );
856 copyTarget = document.getElementById( 'frm-insert-fields' ).querySelector( '.frm_t' + fieldType );
857 copyTarget = copyTarget.cloneNode( true );
858 copyTarget.classList.add( 'form-field' );
859
860 copyTarget.classList.add( 'ui-sortable-helper' );
861
862 if ( copyTarget ) {
863 return copyTarget.cloneNode( true );
864 }
865 }
866
867 return div({ className: 'frmbutton' });
868 }
869
870 function handleDragStart( event, ui ) {
871 dragState.dragging = true;
872
873 const container = postBodyContent;
874 container.classList.add( 'frm-dragging-field' );
875
876 document.body.classList.add( 'frm-dragging' );
877 ui.helper.addClass( 'frm-sortable-helper' );
878 ui.helper.initialOffset = container.scrollTop;
879
880 event.target.classList.add( 'frm-drag-fade' );
881
882 unselectFieldGroups();
883 deleteEmptyDividerWrappers();
884 maybeRemoveGroupHoverTarget();
885 closeOpenFieldDropdowns();
886 deleteTooltips();
887 }
888
889 function handleDragStop() {
890 const container = postBodyContent;
891 container.classList.remove( 'frm-dragging-field' );
892 document.body.classList.remove( 'frm-dragging' );
893
894 const fade = document.querySelector( '.frm-drag-fade' );
895 if ( fade ) {
896 fade.classList.remove( 'frm-drag-fade' );
897 }
898 }
899
900 function handleDrag( event, ui ) {
901 maybeScrollBuilder( event );
902 const draggable = event.target;
903 const droppable = getDroppableTarget();
904
905 let placeholder = document.getElementById( 'frm_drag_placeholder' );
906 if ( ! allowDrop( draggable, droppable, event ) ) {
907 if ( placeholder ) {
908 placeholder.remove();
909 }
910 return;
911 }
912
913 if ( ! placeholder ) {
914 placeholder = tag( 'li', {
915 id: 'frm_drag_placeholder',
916 className: 'sortable-placeholder'
917 });
918 }
919 const frmSortableHelper = ui.helper.get( 0 );
920 if ( frmSortableHelper.classList.contains( 'form-field' ) || frmSortableHelper.classList.contains( 'frm_field_box' ) ) {
921 // Sync the y position of the draggable so it still follows the cursor after scrolling up and down the field list.
922 frmSortableHelper.style.transform = 'translateY(' + getDragOffset( ui.helper ) + 'px)';
923 }
924
925 if ( 'frm-show-fields' === droppable.id || droppable.classList.contains( 'start_divider' ) ) {
926 placeholder.style.left = 0;
927 handleDragOverYAxis({ droppable, y: event.clientY, placeholder });
928 return;
929 }
930
931 placeholder.style.top = '';
932 handleDragOverFieldGroup({ droppable, x: event.clientX, placeholder });
933 }
934
935 function maybeScrollBuilder( event ) {
936 $postBodyContent.scrollTop(
937 ( _, v ) => {
938 const moved = event.clientY;
939 const h = postBodyContent.offsetHeight;
940 const relativePos = event.clientY - postBodyContent.offsetTop;
941 const y = relativePos - h / 2;
942
943 if ( relativePos > ( h - 50 ) && moved > 5 ) {
944 // Scrolling down.
945 return v + y * 0.1;
946 }
947
948 if ( relativePos < 70 && moved < 130 ) {
949 // Scrolling up.
950 return v - Math.abs( y * 0.1 );
951 }
952
953 return v;
954 }
955 );
956 }
957
958 function getDragOffset( $helper ) {
959 return postBodyContent.scrollTop - $helper.initialOffset;
960 }
961
962 function getDroppableTarget() {
963 let droppable = document.getElementById( 'frm-show-fields' );
964 while ( droppable.querySelector( '.frm-over-droppable' ) ) {
965 droppable = droppable.querySelector( '.frm-over-droppable' );
966 }
967 if ( 'frm-show-fields' === droppable.id && ! droppable.classList.contains( 'frm-over-droppable' ) ) {
968 droppable = false;
969 }
970 return droppable;
971 }
972
973 function handleFieldDrop( _, ui ) {
974 if ( ! dragState.dragging ) {
975 // dragState.dragging is set to true on drag start.
976 // The deactivate event gets called for every droppable. This check to make sure it happens once.
977 return;
978 }
979
980 dragState.dragging = false;
981
982 const draggable = ui.draggable[0];
983 const placeholder = document.getElementById( 'frm_drag_placeholder' );
984
985 if ( ! placeholder ) {
986 ui.helper.remove();
987 debouncedSyncAfterDragAndDrop();
988 return;
989 }
990
991 maybeOpenCollapsedPage( placeholder );
992
993 const $previousFieldContainer = ui.helper.parent();
994 const previousSection = ui.helper.get( 0 ).closest( 'ul.start_divider' );
995 const newSection = placeholder.closest( 'ul.frm_sorting' );
996
997 if ( draggable.classList.contains( 'frm-new-field' ) ) {
998 insertNewFieldByDragging( draggable.id );
999 } else {
1000 moveFieldThatAlreadyExists( draggable, placeholder );
1001 }
1002
1003 const previousSectionId = previousSection ? parseInt( previousSection.closest( '.edit_field_type_divider' ).getAttribute( 'data-fid' ) ) : 0;
1004 const newSectionId = newSection.classList.contains( 'start_divider' ) ? parseInt( newSection.closest( '.edit_field_type_divider' ).getAttribute( 'data-fid' ) ) : 0;
1005
1006 placeholder.remove();
1007 ui.helper.remove();
1008
1009 const $previousContainerFields = $previousFieldContainer.length ? getFieldsInRow( $previousFieldContainer ) : [];
1010 maybeUpdatePreviousFieldContainerAfterDrop( $previousFieldContainer, $previousContainerFields );
1011 maybeUpdateDraggableClassAfterDrop( draggable, $previousContainerFields );
1012
1013 if ( previousSectionId !== newSectionId ) {
1014 updateFieldAfterMovingBetweenSections( jQuery( draggable ), previousSection );
1015 }
1016
1017 debouncedSyncAfterDragAndDrop();
1018 }
1019
1020 /**
1021 * If a page if collapsed, expand it before dragging since only the page break will move.
1022 *
1023 * @param {Element} placeholder
1024 * @returns {void}
1025 */
1026 function maybeOpenCollapsedPage( placeholder ) {
1027 if ( ! placeholder.previousElementSibling || ! placeholder.previousElementSibling.classList.contains( 'frm-is-collapsed' ) ) {
1028 return;
1029 }
1030
1031 const $pageBreakField = jQuery( placeholder ).prevUntil( '[data-type="break"]' );
1032 if ( ! $pageBreakField.length ) {
1033 return;
1034 }
1035
1036 const collapseButton = $pageBreakField.find( '.frm-collapse-page' ).get( 0 );
1037 if ( collapseButton ) {
1038 collapseButton.click();
1039 }
1040 }
1041
1042 function maybeUpdatePreviousFieldContainerAfterDrop( $previousFieldContainer, $previousContainerFields ) {
1043 if ( ! $previousFieldContainer.length ) {
1044 return;
1045 }
1046
1047 if ( $previousContainerFields.length ) {
1048 syncLayoutClasses( $previousContainerFields.first() );
1049 } else {
1050 maybeDeleteAnEmptyFieldGroup( $previousFieldContainer.get( 0 ) );
1051 }
1052 }
1053
1054 function maybeUpdateDraggableClassAfterDrop( draggable, $previousContainerFields ) {
1055 if ( 0 !== $previousContainerFields.length || 1 !== getFieldsInRow( jQuery( draggable.parentNode ) ).length ) {
1056 syncLayoutClasses( jQuery( draggable ) );
1057 }
1058 }
1059
1060 /**
1061 * Remove an empty field group, but don't remove an empty section.
1062 *
1063 * @param {Element} previousFieldContainer
1064 * @returns {void}
1065 */
1066 function maybeDeleteAnEmptyFieldGroup( previousFieldContainer ) {
1067 const closestFieldBox = previousFieldContainer.closest( 'li.frm_field_box' );
1068 if ( closestFieldBox && ! closestFieldBox.classList.contains( 'edit_field_type_divider' ) ) {
1069 closestFieldBox.remove();
1070 }
1071 }
1072
1073 function handleDragOverYAxis({ droppable, y, placeholder }) {
1074 const $list = jQuery( droppable );
1075
1076 let top;
1077
1078 $children = $list.children().not( '.edit_field_type_end_divider' );
1079 if ( 0 === $children.length ) {
1080 $list.prepend( placeholder );
1081 top = 0;
1082 } else {
1083 const insertAtIndex = determineIndexBasedOffOfMousePositionInList( $list, y );
1084
1085 if ( insertAtIndex === $children.length ) {
1086 const $lastChild = jQuery( $children.get( insertAtIndex - 1 ) );
1087 top = $lastChild.offset().top + $lastChild.outerHeight();
1088 $list.append( placeholder );
1089
1090 // Make sure nothing gets inserted after the end divider.
1091 const $endDivider = $list.children( '.edit_field_type_end_divider' );
1092 if ( $endDivider.length ) {
1093 $list.append( $endDivider );
1094 }
1095 } else {
1096 top = jQuery( $children.get( insertAtIndex ) ).offset().top;
1097 jQuery( $children.get( insertAtIndex ) ).before( placeholder );
1098 }
1099 }
1100
1101 top -= $list.offset().top;
1102 placeholder.style.top = top + 'px';
1103 }
1104
1105 function determineIndexBasedOffOfMousePositionInList( $list, y ) {
1106 const $items = $list.children().not( '.edit_field_type_end_divider' );
1107 const length = $items.length;
1108
1109 let index, item, itemTop, returnIndex;
1110
1111 if ( ! document.querySelector( '.frm-has-fields .frm_no_fields' ) ) {
1112 // Always return 0 when there are no fields.
1113 return 0;
1114 }
1115
1116 returnIndex = 0;
1117 for ( index = length - 1; index >= 0; --index ) {
1118 item = $items.get( index );
1119 itemTop = jQuery( item ).offset().top;
1120 if ( y > itemTop ) {
1121 returnIndex = index;
1122 if ( y > itemTop + ( jQuery( item ).outerHeight() / 2 ) ) {
1123 returnIndex = index + 1;
1124 }
1125 break;
1126 }
1127 }
1128
1129 return returnIndex;
1130 }
1131
1132 function handleDragOverFieldGroup({ droppable, x, placeholder }) {
1133 const $row = jQuery( droppable );
1134 const $children = getFieldsInRow( $row );
1135
1136 if ( ! $children.length ) {
1137 return;
1138 }
1139
1140 let left;
1141 const insertAtIndex = determineIndexBasedOffOfMousePositionInRow( $row, x );
1142
1143 if ( insertAtIndex === $children.length ) {
1144 const $lastChild = jQuery( $children.get( insertAtIndex - 1 ) );
1145 left = $lastChild.offset().left + $lastChild.outerWidth();
1146 $row.append( placeholder );
1147 } else {
1148 left = jQuery( $children.get( insertAtIndex ) ).offset().left;
1149 jQuery( $children.get( insertAtIndex ) ).before( placeholder );
1150
1151 const amountToOffsetLeftBy = 0 === insertAtIndex ? 4 : 8; // Offset by 8 in between rows, but only 4 for the first item in a group.
1152 left -= amountToOffsetLeftBy; // Offset the placeholder slightly so it appears between two fields.
1153 }
1154
1155 left -= $row.offset().left;
1156
1157 placeholder.style.left = left + 'px';
1158 }
1159
1160 function syncAfterDragAndDrop() {
1161 fixUnwrappedListItems();
1162 toggleSectionHolder();
1163 maybeFixEndDividers();
1164 maybeDeleteEmptyFieldGroups();
1165 updateFieldOrder();
1166
1167 const event = new Event( 'frm_sync_after_drag_and_drop', { bubbles: false });
1168 document.dispatchEvent( event );
1169 }
1170
1171 function maybeFixEndDividers() {
1172 document.querySelectorAll( '.edit_field_type_end_divider' ).forEach(
1173 endDivider => endDivider.parentNode.appendChild( endDivider )
1174 );
1175 }
1176
1177 function maybeDeleteEmptyFieldGroups() {
1178 document.querySelectorAll( 'li.form_field_box:not(.form-field)' ).forEach(
1179 fieldGroup => ! fieldGroup.children.length && fieldGroup.remove()
1180 );
1181 }
1182
1183 function fixUnwrappedListItems() {
1184 const lists = document.querySelectorAll( 'ul#frm-show-fields, ul.start_divider' );
1185 lists.forEach(
1186 list => {
1187 list.childNodes.forEach(
1188 child => {
1189 if ( 'undefined' === typeof child.classList ) {
1190 return;
1191 }
1192
1193 if ( child.classList.contains( 'edit_field_type_end_divider' ) ) {
1194 // Never wrap end divider in place.
1195 return;
1196 }
1197
1198 if ( 'undefined' !== typeof child.classList && child.classList.contains( 'form-field' ) ) {
1199 wrapFieldLiInPlace( child );
1200 }
1201 }
1202 );
1203 }
1204 );
1205 }
1206
1207 function deleteEmptyDividerWrappers() {
1208 const dividers = document.querySelectorAll( 'ul.start_divider' );
1209 if ( ! dividers.length ) {
1210 return;
1211 }
1212 dividers.forEach(
1213 function( divider ) {
1214 const children = [].slice.call( divider.children );
1215 children.forEach(
1216 function( child ) {
1217 if ( 0 === child.children.length ) {
1218 child.remove();
1219 } else if ( 1 === child.children.length && 'ul' === child.firstElementChild.nodeName.toLowerCase() && 0 === child.firstElementChild.children.length ) {
1220 child.remove();
1221 }
1222 }
1223 );
1224 }
1225 );
1226 }
1227
1228 function getFieldsInRow( $row ) {
1229 let $fields = jQuery();
1230
1231 const row = $row.get( 0 );
1232 if ( ! row.children ) {
1233 return $fields;
1234 }
1235
1236 Array.from( row.children ).forEach(
1237 child => {
1238 if ( 'none' === child.style.display ) {
1239 return;
1240 }
1241
1242 const classes = child.classList;
1243 if ( ! classes.contains( 'form-field' ) || classes.contains( 'edit_field_type_end_divider' ) || classes.contains( 'frm-sortable-helper' ) ) {
1244 return;
1245 }
1246
1247 $fields = $fields.add( child );
1248 }
1249 );
1250 return $fields;
1251 }
1252
1253 function determineIndexBasedOffOfMousePositionInRow( $row, x ) {
1254 let $inputs = getFieldsInRow( $row ),
1255 length = $inputs.length,
1256 index, input, inputLeft, returnIndex;
1257
1258 returnIndex = 0;
1259 for ( index = length - 1; index >= 0; --index ) {
1260 input = $inputs.get( index );
1261 inputLeft = jQuery( input ).offset().left;
1262 if ( x > inputLeft ) {
1263 returnIndex = index;
1264 if ( x > inputLeft + ( jQuery( input ).outerWidth() / 2 ) ) {
1265 returnIndex = index + 1;
1266 }
1267 break;
1268 }
1269 }
1270
1271 return returnIndex;
1272 }
1273
1274 function syncLayoutClasses( $item, type ) {
1275 let $fields, size, layoutClasses, classToAddFunction;
1276
1277 if ( 'undefined' === typeof type ) {
1278 type = 'even';
1279 }
1280
1281 $fields = $item.parent().children( 'li.form-field, li.frmbutton_loadingnow' ).not( '.edit_field_type_end_divider' );
1282 size = $fields.length;
1283 layoutClasses = getLayoutClasses();
1284
1285 if ( 'even' === type && 5 !== size ) {
1286 $fields.each( getSyncLayoutClass( layoutClasses, getEvenClassForSize( size ) ) );
1287 } else if ( 'clear' === type ) {
1288 $fields.each( getSyncLayoutClass( layoutClasses, '' ) );
1289 } else {
1290 if ( -1 !== [ 'left', 'right', 'middle', 'even' ].indexOf( type ) ) {
1291 classToAddFunction = function( index ) {
1292 return getClassForBlock( size, type, index );
1293 };
1294 } else {
1295 classToAddFunction = function( index ) {
1296 const size = type[ index ];
1297 return getLayoutClassForSize( size );
1298 };
1299 }
1300
1301 $fields.each( getSyncLayoutClass( layoutClasses, classToAddFunction ) );
1302 }
1303
1304 updateFieldGroupControls( $item.parent(), $fields.length );
1305 }
1306
1307 function updateFieldGroupControls( $row, count ) {
1308 let rowOffset, shouldShowControls, controls;
1309
1310 rowOffset = $row.offset();
1311
1312 if ( 'undefined' === typeof rowOffset ) {
1313 return;
1314 }
1315
1316 shouldShowControls = count >= 2;
1317
1318 controls = document.getElementById( 'frm_field_group_controls' );
1319 if ( null === controls ) {
1320 if ( ! shouldShowControls ) {
1321 // exit early. if we do not need controls and they do not exist, do nothing.
1322 return;
1323 }
1324
1325 controls = div();
1326 controls.id = 'frm_field_group_controls';
1327 controls.setAttribute( 'role', 'group' );
1328 controls.setAttribute( 'tabindex', 0 );
1329 setFieldControlsHtml( controls );
1330 builderPage.appendChild( controls );
1331 }
1332
1333 $row.append( controls );
1334 controls.style.display = shouldShowControls ? 'block' : 'none';
1335 }
1336
1337 function setFieldControlsHtml( controls ) {
1338 let layoutOption, moveOption;
1339
1340 layoutOption = document.createElement( 'span' );
1341 layoutOption.innerHTML = '<svg class="frmsvg"><use xlink:href="#frm_field_group_layout_icon"></use></svg>';
1342 const layoutOptionLabel = __( 'Set Row Layout', 'formidable' );
1343 addTooltip( layoutOption, layoutOptionLabel );
1344 makeTabbable( layoutOption, layoutOptionLabel );
1345
1346 moveOption = document.createElement( 'span' );
1347 moveOption.innerHTML = '<svg class="frmsvg"><use xlink:href="#frm_thick_move_icon"></use></svg>';
1348 moveOption.classList.add( 'frm-move' );
1349 const moveOptionLabel = __( 'Move Field Group', 'formidable' );
1350 addTooltip( moveOption, moveOptionLabel );
1351 makeTabbable( moveOption, moveOptionLabel );
1352
1353 controls.innerHTML = '';
1354 controls.appendChild( layoutOption );
1355 controls.appendChild( moveOption );
1356 controls.appendChild( getFieldControlsDropdown() );
1357 }
1358
1359 function addTooltip( element, title ) {
1360 element.setAttribute( 'data-toggle', 'tooltip' );
1361 element.setAttribute( 'data-container', 'body' );
1362 element.setAttribute( 'title', title );
1363 element.addEventListener(
1364 'mouseover',
1365 function() {
1366 if ( null === element.getAttribute( 'data-original-title' ) ) {
1367 jQuery( element ).tooltip();
1368 }
1369 }
1370 );
1371 }
1372
1373 function getFieldControlsDropdown() {
1374 const dropdown = span({ className: 'dropdown' });
1375 const trigger = a({
1376 className: 'frm_bstooltip frm-hover-icon frm-dropdown-toggle dropdown-toggle',
1377 children: [
1378 span({
1379 child: svg({ href: '#frm_thick_more_vert_icon' })
1380 }),
1381 span({
1382 className: 'screen-reader-text',
1383 text: __( 'Toggle More Options Dropdown', 'formidable' )
1384 })
1385 ]
1386 });
1387
1388 frmDom.setAttributes(
1389 trigger,
1390 {
1391 'title': __( 'More Options', 'formidable' ),
1392 'data-toggle': 'dropdown',
1393 'data-container': 'body'
1394 }
1395 );
1396 makeTabbable( trigger, __( 'More Options', 'formidable' ) );
1397 dropdown.appendChild( trigger );
1398
1399 const ul = div({
1400 className: 'frm-dropdown-menu dropdown-menu dropdown-menu-right'
1401 });
1402 ul.setAttribute( 'role', 'menu' );
1403 dropdown.appendChild( ul );
1404
1405 return dropdown;
1406 }
1407
1408 function getSyncLayoutClass( layoutClasses, classToAdd ) {
1409 return function( itemIndex ) {
1410 let currentClassToAdd, length, layoutClassIndex, currentClass, activeLayoutClass, fieldId, layoutClassesInput;
1411
1412 currentClassToAdd = 'function' === typeof classToAdd ? classToAdd( itemIndex ) : classToAdd;
1413 length = layoutClasses.length;
1414 activeLayoutClass = false;
1415 for ( layoutClassIndex = 0; layoutClassIndex < length; ++layoutClassIndex ) {
1416 currentClass = layoutClasses[ layoutClassIndex ];
1417 if ( this.classList.contains( currentClass ) ) {
1418 activeLayoutClass = currentClass;
1419 break;
1420 }
1421 }
1422
1423 fieldId = this.dataset.fid;
1424
1425 if ( 'undefined' === typeof fieldId ) {
1426 // we are syncing the drag/drop placeholder before the actual field has loaded.
1427 // this will get called again afterward and the input will exist then.
1428 this.classList.add( currentClassToAdd );
1429 return;
1430 }
1431
1432 moveFieldSettings( document.getElementById( 'frm-single-settings-' + fieldId ) );
1433 layoutClassesInput = document.getElementById( 'frm_classes_' + fieldId );
1434
1435 if ( null === layoutClassesInput ) {
1436 // not every field type has a layout class input.
1437 return;
1438 }
1439
1440 if ( false === activeLayoutClass ) {
1441 if ( '' !== currentClassToAdd ) {
1442 layoutClassesInput.value = layoutClassesInput.value.concat( ' ' + currentClassToAdd );
1443 }
1444 } else {
1445 this.classList.remove( activeLayoutClass );
1446 layoutClassesInput.value = layoutClassesInput.value.replace( activeLayoutClass, currentClassToAdd );
1447 }
1448
1449 if ( this.classList.contains( 'frm_first' ) ) {
1450 this.classList.remove( 'frm_first' );
1451 layoutClassesInput.value = layoutClassesInput.value.replace( 'frm_first', '' ).trim();
1452 }
1453
1454 if ( 0 === itemIndex ) {
1455 this.classList.add( 'frm_first' );
1456 layoutClassesInput.value = layoutClassesInput.value.concat( ' frm_first' );
1457 }
1458
1459 jQuery( layoutClassesInput ).trigger( 'change' );
1460 };
1461 }
1462
1463 function getLayoutClasses() {
1464 return [ 'frm_full', 'frm_half', 'frm_third', 'frm_fourth', 'frm_sixth', 'frm_two_thirds', 'frm_three_fourths', 'frm1', 'frm2', 'frm3', 'frm4', 'frm5', 'frm6', 'frm7', 'frm8', 'frm9', 'frm10', 'frm11', 'frm12' ];
1465 }
1466
1467 function setupFieldOptionSorting( sort ) {
1468 const opts = {
1469 items: '.frm_sortable_field_opts li',
1470 axis: 'y',
1471 opacity: 0.65,
1472 forcePlaceholderSize: false,
1473 handle: '.frm-drag',
1474 helper: function( e, li ) {
1475 copyHelper = li.clone().insertAfter( li );
1476 return li.clone();
1477 },
1478 stop: function( e, ui ) {
1479 copyHelper && copyHelper.remove();
1480 const fieldId = ui.item.attr( 'id' ).replace( 'frm_delete_field_', '' ).replace( '-' + ui.item.data( 'optkey' ) + '_container', '' );
1481 resetDisplayedOpts( fieldId );
1482 fieldUpdated();
1483 }
1484 };
1485 jQuery( sort ).sortable( opts );
1486 }
1487
1488 // Get the section where a field is dropped
1489 function getSectionForFieldPlacement( currentItem ) {
1490 let section = '';
1491 if ( typeof currentItem !== 'undefined' && ! currentItem.hasClass( 'edit_field_type_divider' ) ) {
1492 section = currentItem.closest( '.edit_field_type_divider' );
1493 }
1494 return section;
1495 }
1496
1497 // Get the form ID where a field is dropped
1498 function getFormIdForFieldPlacement( section ) {
1499 let formId = '';
1500
1501 if ( typeof section[0] !== 'undefined' ) {
1502 const sDivide = section.children( '.start_divider' );
1503 sDivide.children( '.edit_field_type_end_divider' ).appendTo( sDivide );
1504 if ( typeof section.attr( 'data-formid' ) !== 'undefined' ) {
1505 const fieldId = section.attr( 'data-fid' );
1506 formId = jQuery( 'input[name="field_options[form_select_' + fieldId + ']"]' ).val();
1507 }
1508 }
1509
1510 if ( typeof formId === 'undefined' || formId === '' ) {
1511 formId = thisFormId;
1512 }
1513
1514 return formId;
1515 }
1516
1517 // Get the section ID where a field is dropped
1518 function getSectionIdForFieldPlacement( section ) {
1519 let sectionId = 0;
1520 if ( typeof section[0] !== 'undefined' ) {
1521 sectionId = section.attr( 'id' ).replace( 'frm_field_id_', '' );
1522 }
1523
1524 return sectionId;
1525 }
1526
1527 /**
1528 * Update a field after it is dragged and dropped into, out of, or between sections
1529 *
1530 * @param {Object} currentItem
1531 * @param {Object} previousSection
1532 * @returns {void}
1533 */
1534 function updateFieldAfterMovingBetweenSections( currentItem, previousSection ) {
1535 if ( ! currentItem.hasClass( 'form-field' ) ) {
1536 // currentItem is a field group. Call for children recursively.
1537 getFieldsInRow( jQuery( currentItem.get( 0 ).firstChild ) ).each(
1538 function() {
1539 updateFieldAfterMovingBetweenSections( jQuery( this ), previousSection );
1540 }
1541 );
1542 return;
1543 }
1544
1545 const fieldId = currentItem.attr( 'id' ).replace( 'frm_field_id_', '' );
1546 const section = getSectionForFieldPlacement( currentItem );
1547 const formId = getFormIdForFieldPlacement( section );
1548 const sectionId = getSectionIdForFieldPlacement( section );
1549 const previousFormId = previousSection ? getFormIdForFieldPlacement( jQuery( previousSection.parentNode ) ) : 0;
1550
1551 jQuery.ajax({
1552 type: 'POST',
1553 url: ajaxurl,
1554 data: {
1555 action: 'frm_update_field_after_move',
1556 form_id: formId,
1557 field: fieldId,
1558 section_id: sectionId,
1559 previous_form_id: previousFormId,
1560 nonce: frmGlobal.nonce
1561 },
1562 success: function() {
1563 toggleSectionHolder();
1564 updateInSectionValue( fieldId, sectionId );
1565 }
1566 });
1567 }
1568
1569 // Update the in_section field value
1570 function updateInSectionValue( fieldId, sectionId ) {
1571 document.getElementById( 'frm_in_section_' + fieldId ).value = sectionId;
1572 }
1573
1574 /**
1575 * Get the arguments for inserting a new field.
1576 *
1577 * @since 6.23
1578 *
1579 * @param {string} fieldType
1580 * @param {string} sectionId
1581 * @param {string} formId
1582 * @param {Number} hasBreak
1583 *
1584 * @returns {Object}
1585 */
1586 function getInsertNewFieldArgs( fieldType, sectionId, formId, hasBreak ) {
1587 return {
1588 action: 'frm_insert_field',
1589 form_id: formId,
1590 field_type: fieldType,
1591 section_id: sectionId,
1592 nonce: frmGlobal.nonce,
1593 has_break: hasBreak,
1594 last_row_field_ids: getFieldIdsInSubmitRow()
1595 };
1596 }
1597
1598 /**
1599 * Returns true if it's a range field type and slider type is not selected.
1600 *
1601 * @since 6.23
1602 *
1603 * @param {string} fieldType
1604 * @returns {boolean}
1605 */
1606 function shouldStopInsertingField( fieldType ) {
1607 return wp.hooks.applyFilters( 'frm_should_stop_inserting_field', false, fieldType );
1608 }
1609
1610 /**
1611 * Add a new field by dragging and dropping it from the Fields sidebar
1612 *
1613 * @param {string} fieldType
1614 */
1615 function insertNewFieldByDragging( fieldType ) {
1616 if ( shouldStopInsertingField( fieldType ) ) {
1617 wp.hooks.doAction( 'frm_stopped_inserting_by_dragging', fieldType );
1618 return;
1619 }
1620
1621 const placeholder = document.getElementById( 'frm_drag_placeholder' );
1622 const loadingID = fieldType.replace( '|', '-' ) + '_' + getAutoId();
1623 const loading = tag(
1624 'li',
1625 {
1626 id: loadingID,
1627 className: 'frm-wait frmbutton_loadingnow'
1628 }
1629 );
1630 const $placeholder = jQuery( loading );
1631 const currentItem = jQuery( placeholder );
1632 const section = getSectionForFieldPlacement( currentItem );
1633 const formId = getFormIdForFieldPlacement( section );
1634 const sectionId = getSectionIdForFieldPlacement( section );
1635
1636 placeholder.parentNode.insertBefore( loading, placeholder );
1637 placeholder.remove();
1638 syncLayoutClasses( $placeholder );
1639
1640 let hasBreak = 0;
1641 if ( 'summary' === fieldType ) {
1642 // see if we need to insert a page break before this newly-added summary field. Check for at least 1 page break
1643 hasBreak = jQuery( '.frmbutton_loadingnow#' + loadingID ).prevAll( 'li[data-type="break"]' ).length ? 1 : 0;
1644 }
1645
1646 jQuery.ajax({
1647 type: 'POST',
1648 url: ajaxurl,
1649 data: getInsertNewFieldArgs( fieldType, sectionId, formId, hasBreak ),
1650 success: function( msg ) {
1651 handleInsertFieldByDraggingResponse( msg, $placeholder );
1652
1653 const fieldId = checkMsgForFieldId( msg );
1654 if ( fieldId ) {
1655 /**
1656 * Fires after a field is added.
1657 *
1658 * @since 6.23
1659 *
1660 * @param {Object} fieldData The field data.
1661 * @param {String} fieldData.field The field HTML.
1662 * @param {String} fieldData.field_type The field type.
1663 * @param {String} fieldData.form_id The form ID.
1664 */
1665 wp.hooks.doAction( 'frm_after_field_added_in_form_builder', {
1666 field: msg,
1667 fieldId,
1668 fieldType,
1669 form_id: formId,
1670 });
1671 }
1672 },
1673 error: handleInsertFieldError
1674 });
1675 }
1676
1677 /**
1678 * @param {String} msg
1679 * @param {Object} $placeholder jQuery object.
1680 */
1681 function handleInsertFieldByDraggingResponse( msg, $placeholder ) {
1682 let replaceWith;
1683 document.getElementById( 'frm_form_editor_container' ).classList.add( 'frm-has-fields' );
1684 const $siblings = $placeholder.siblings( 'li.form-field' ).not( '.edit_field_type_end_divider' );
1685
1686 if ( ! $siblings.length ) {
1687 // if dragging into a new row, we need to wrap the li first.
1688 replaceWith = wrapFieldLi( msg );
1689 } else {
1690 replaceWith = msgAsjQueryObject( msg );
1691 if ( ! $placeholder.get( 0 ).parentNode.parentNode.classList.contains( 'ui-draggable' ) ) {
1692 // If a field group wasn't draggable because it only had a single field, make it draggable.
1693 makeDraggable( $placeholder.get( 0 ).parentNode.parentNode, '.frm-move' );
1694 }
1695 }
1696 $placeholder.replaceWith( replaceWith );
1697 updateFieldOrder();
1698 afterAddField( msg, false );
1699 if ( $siblings.length ) {
1700 syncLayoutClasses( $siblings.first() );
1701 }
1702 toggleSectionHolder();
1703
1704 if ( ! $siblings.length ) {
1705 makeDroppable( replaceWith.get( 0 ).querySelector( 'ul.frm_sorting' ) );
1706 makeDraggable( replaceWith.get( 0 ).querySelector( 'li.form-field' ), '.frm-move' );
1707 } else {
1708 makeDraggable( replaceWith.get( 0 ), '.frm-move' );
1709 }
1710 }
1711
1712 /**
1713 * Get the field ID from the response message.
1714 *
1715 * @since 6.23
1716 *
1717 * @param {String} msg
1718 * @return {Number}
1719 */
1720 function checkMsgForFieldId( msg ) {
1721 const result = msg.match( /data-fid="(\d+)"/ );
1722 return result ? parseInt( result[1] ) : 0;
1723 }
1724
1725 function getFieldIdsInSubmitRow() {
1726 const submitField = document.querySelector( '.edit_field_type_submit' );
1727 if ( ! submitField ) {
1728 return [];
1729 }
1730
1731 const lastRowFields = submitField.parentNode.children;
1732 const ids = [];
1733 for ( let i = 0; i < lastRowFields.length; i++ ) {
1734 ids.push( lastRowFields[ i ].dataset.fid );
1735 }
1736
1737 return ids;
1738 }
1739
1740 function moveFieldThatAlreadyExists( draggable, placeholder ) {
1741 placeholder.parentNode.insertBefore( draggable, placeholder );
1742 }
1743
1744 function msgAsjQueryObject( msg ) {
1745 const element = div();
1746 element.innerHTML = msg;
1747 return jQuery( element.firstChild );
1748 }
1749
1750 function handleInsertFieldError( jqXHR, _, errorThrown ) {
1751 maybeShowInsertFieldError( errorThrown, jqXHR );
1752 }
1753
1754 function maybeShowInsertFieldError( errorThrown, jqXHR ) {
1755 if ( ! jqXHRAborted( jqXHR ) ) {
1756 infoModal( errorThrown + '. Please try again.' );
1757 }
1758 }
1759
1760 function jqXHRAborted( jqXHR ) {
1761 return jqXHR.status === 0 || jqXHR.readyState === 0;
1762 }
1763
1764 /**
1765 * Get a unique id that automatically increments with every function call.
1766 * Can be used for any UI that requires a unique id.
1767 * Not to be used in data.
1768 *
1769 * @returns {integer}
1770 */
1771 function getAutoId() {
1772 return ++autoId;
1773 }
1774
1775 /**
1776 * Determine if a draggable element can be droppable into a droppable element.
1777 *
1778 * Don't allow page break, embed form, or section inside section field
1779 * Don't allow page breaks inside of field groups.
1780 * Don't allow field groups with sections inside of sections.
1781 * Don't allow field groups in field groups.
1782 * Don't allow hidden fields inside of field groups but allow them in sections.
1783 * Don't allow any fields below the submit button field.
1784 * Don't allow submit button field above any fields.
1785 * Don't allow GDPR fields in repeaters.
1786 *
1787 * @param {HTMLElement} draggable
1788 * @param {HTMLElement} droppable
1789 * @param {Event} event
1790 * @returns {Boolean}
1791 */
1792 function allowDrop( draggable, droppable, event ) {
1793 if ( false === droppable ) {
1794 // Don't show drop placeholder if dragging somewhere off of the droppable area.
1795 return false;
1796 }
1797
1798 if ( droppable.closest( '.frm-sortable-helper' ) ) {
1799 // Do not allow drop into draggable.
1800 return false;
1801 }
1802
1803 const isSubmitBtn = draggable.classList.contains( 'edit_field_type_submit' );
1804 const containSubmitBtn = ! draggable.classList.contains( 'form_field' ) && !! draggable.querySelector( '.edit_field_type_submit' );
1805
1806 if ( 'frm-show-fields' === droppable.id ) {
1807 const draggableIndex = determineIndexBasedOffOfMousePositionInList( jQuery( droppable ), event.clientY );
1808
1809 if ( isSubmitBtn || containSubmitBtn ) {
1810 // Do not allow dropping submit button to above position.
1811 const lastRowIndex = droppable.childElementCount - 1;
1812 return draggableIndex > lastRowIndex;
1813 }
1814
1815 // Do not allow dropping other fields to below submit button.
1816 const submitButtonIndex = jQuery( droppable.querySelector( '.edit_field_type_submit' ).closest( '#frm-show-fields > li' ) ).index();
1817 return draggableIndex <= submitButtonIndex;
1818 }
1819
1820 if ( isSubmitBtn ) {
1821 if ( droppable.classList.contains( 'start_divider' ) ) {
1822 // Don't allow dropping submit button into a repeater.
1823 return false;
1824 }
1825
1826 if ( isLastRow( droppable.parentElement ) ) {
1827 // Allow dropping submit button into the last row.
1828 return true;
1829 }
1830
1831 if ( ! isLastRow( droppable.parentElement.nextElementSibling ) ) {
1832 // Don't a dropping submit button into the row that isn't the second one from bottom.
1833 return false;
1834 }
1835
1836 // Allow dropping submit button into the second row from bottom if there is only submit button in the last row.
1837 return ! draggable.parentElement.querySelector( 'li.frm_field_box:not(.edit_field_type_submit)' );
1838 }
1839
1840 if ( droppable.classList.contains( 'start_divider' ) && ( draggable.classList.contains( 'edit_field_type_gdpr' ) || draggable.id === 'gdpr' ) && droppable.closest( '.repeat_section' ) ) {
1841 // Don't allow GDPR fields in repeaters.
1842 return false;
1843 }
1844
1845 if ( ! droppable.classList.contains( 'start_divider' ) ) {
1846 const $fieldsInRow = getFieldsInRow( jQuery( droppable ) );
1847 if ( ! groupCanFitAnotherField( $fieldsInRow, jQuery( draggable ) ) ) {
1848 // Field group is full and cannot accept another field.
1849 return false;
1850 }
1851
1852 if ( draggable.id === 'divider' && droppable.closest( '.start_divider' ) ) {
1853 return false;
1854 }
1855 }
1856
1857 const isNewField = draggable.classList.contains( 'frm-new-field' );
1858 if ( isNewField ) {
1859 return allowNewFieldDrop( draggable, droppable );
1860 }
1861
1862 return allowMoveField( draggable, droppable );
1863 }
1864
1865 /**
1866 * Checks if given element is the last row in form builder.
1867 *
1868 * @param {HTMLElement} element Element.
1869 * @return {Boolean}
1870 */
1871 function isLastRow( element ) {
1872 return element && element.matches( '#frm-show-fields > li:last-child' );
1873 }
1874
1875 // Don't allow a new page break or hidden field in a field group.
1876 // Don't allow a new field into a field group that includes a page break or hidden field.
1877 // Don't allow a new section inside of a section.
1878 // Don't allow an embedded form in a section.
1879 function allowNewFieldDrop( draggable, droppable ) {
1880 const classes = draggable.classList;
1881 const newPageBreakField = classes.contains( 'frm_tbreak' );
1882 const newHiddenField = classes.contains( 'frm_thidden' );
1883 const newSectionField = classes.contains( 'frm_tdivider' );
1884 const newEmbedField = classes.contains( 'frm_tform' );
1885 const newUserIdField = classes.contains( 'frm_tuser_id' );
1886
1887 const newFieldWillBeAddedToAGroup = ! ( 'frm-show-fields' === droppable.id || droppable.classList.contains( 'start_divider' ) );
1888 if ( newFieldWillBeAddedToAGroup ) {
1889 if ( groupIncludesBreakOrHiddenOrUserId( droppable ) ) {
1890 // Never allow any field beside a page break or a hidden field.
1891 return false;
1892 }
1893
1894 return ! newHiddenField && ! newPageBreakField && ! newUserIdField;
1895 }
1896
1897 const fieldTypeIsAlwaysAllowed = ! newPageBreakField && ! newHiddenField && ! newSectionField && ! newEmbedField;
1898 if ( fieldTypeIsAlwaysAllowed ) {
1899 return true;
1900 }
1901
1902 const newFieldWillBeAddedToASection = droppable.classList.contains( 'start_divider' ) || null !== droppable.closest( '.start_divider' );
1903 if ( newFieldWillBeAddedToASection ) {
1904 // Don't allow a section or an embedded form in a section.
1905 return ! newEmbedField && ! newSectionField;
1906 }
1907
1908 return true;
1909 }
1910
1911 function allowMoveField( draggable, droppable ) {
1912 if ( isFieldGroup( draggable ) ) {
1913 return allowMoveFieldGroup( draggable, droppable );
1914 }
1915
1916 const isPageBreak = draggable.classList.contains( 'edit_field_type_break' );
1917 if ( isPageBreak ) {
1918 // Page breaks are only allowed in the main list of fields, not in sections or in field groups.
1919 return false;
1920 }
1921
1922 if ( droppable.classList.contains( 'start_divider' ) ) {
1923 return allowMoveFieldToSection( draggable );
1924 }
1925
1926 const isHiddenField = draggable.classList.contains( 'edit_field_type_hidden' );
1927 const isUserIdField = draggable.classList.contains( 'edit_field_type_user_id' );
1928 if ( isHiddenField || isUserIdField ) {
1929 // Hidden fields and user id fields should not be added to field groups since they're not shown
1930 // and don't make sense with the grid distribution.
1931 return false;
1932 }
1933
1934 return allowMoveFieldToGroup( draggable, droppable );
1935 }
1936
1937 function isFieldGroup( draggable ) {
1938 return draggable.classList.contains( 'frm_field_box' ) && ! draggable.classList.contains( 'form-field' );
1939 }
1940
1941 function allowMoveFieldGroup( fieldGroup, droppable ) {
1942 if ( droppable.classList.contains( 'start_divider' ) && null === fieldGroup.querySelector( '.start_divider' ) ) {
1943 // Allow a field group with no section inside of a section.
1944 return true;
1945 }
1946 return false;
1947 }
1948
1949 function allowMoveFieldToSection( draggable ) {
1950 const draggableIncludeEmbedForm = draggable.classList.contains( 'edit_field_type_form' ) || draggable.querySelector( '.edit_field_type_form' );
1951 if ( draggableIncludeEmbedForm ) {
1952 // Do not allow an embedded form inside of a section.
1953 return false;
1954 }
1955
1956 const draggableIncludesSection = draggable.classList.contains( 'edit_field_type_divider' ) || draggable.querySelector( '.edit_field_type_divider' );
1957 if ( draggableIncludesSection ) {
1958 // Do not allow a section inside of a section.
1959 return false;
1960 }
1961
1962 return true;
1963 }
1964
1965 function allowMoveFieldToGroup( draggable, group ) {
1966 if ( groupIncludesBreakOrHiddenOrUserId( group ) ) {
1967 // Never allow any field beside a page break or a hidden field.
1968 return false;
1969 }
1970
1971 const isFieldGroup = jQuery( draggable ).children( 'ul.frm_sorting' ).not( '.start_divider' ).length > 0;
1972 if ( isFieldGroup ) {
1973 // Do not allow a field group directly inside of a field group unless it's in a section.
1974 return false;
1975 }
1976
1977 const draggableIncludesASection = draggable.classList.contains( 'edit_field_type_divider' ) || draggable.querySelector( '.edit_field_type_divider' );
1978 const draggableIsEmbedField = draggable.classList.contains( 'edit_field_type_form' );
1979 const groupIsInASection = null !== group.closest( '.start_divider' );
1980 if ( groupIsInASection && ( draggableIncludesASection || draggableIsEmbedField ) ) {
1981 // Do not allow a section or an embed field inside of a section.
1982 return false;
1983 }
1984
1985 return true;
1986 }
1987
1988 function groupIncludesBreakOrHiddenOrUserId( group ) {
1989 return null !== group.querySelector( '.edit_field_type_break, .edit_field_type_hidden, .edit_field_type_user_id' );
1990 }
1991
1992 function groupCanFitAnotherField( fieldsInRow, $field ) {
1993 let fieldId;
1994 if ( fieldsInRow.length < 6 ) {
1995 return true;
1996 }
1997 if ( fieldsInRow.length > 6 ) {
1998 return false;
1999 }
2000 fieldId = $field.attr( 'data-fid' );
2001 // allow 6 if we're not changing field groups.
2002 return 1 === jQuery( fieldsInRow ).filter( '[data-fid="' + fieldId + '"]' ).length;
2003 }
2004
2005 function loadFields( fieldId ) {
2006 const thisField = document.getElementById( fieldId );
2007 const $thisField = jQuery( thisField );
2008 const field = [];
2009 const addHtmlToField = element => {
2010 const frmHiddenFdata = element.querySelector( '.frm_hidden_fdata' );
2011 element.classList.add( 'frm_load_now' );
2012 if ( frmHiddenFdata !== null ) {
2013 field.push( frmHiddenFdata.innerHTML );
2014 }
2015 };
2016
2017 let nextElement = thisField;
2018 addHtmlToField( nextElement );
2019
2020 let nextField = getNextField( nextElement );
2021 while ( nextField && field.length < 15 ) {
2022 addHtmlToField( nextField );
2023 nextElement = nextField;
2024 nextField = getNextField( nextField );
2025 }
2026
2027 jQuery.ajax({
2028 type: 'POST',
2029 url: ajaxurl,
2030 data: {
2031 action: 'frm_load_field',
2032 field: field,
2033 form_id: thisFormId,
2034 nonce: frmGlobal.nonce
2035 },
2036 success: html => handleAjaxLoadFieldSuccess( html, $thisField, field )
2037 });
2038 }
2039
2040 function getNextField( field ) {
2041 if ( field.nextElementSibling ) {
2042 return field.nextElementSibling;
2043 }
2044 return field.parentNode?.closest( '.frm_field_box' )?.nextElementSibling?.querySelector( '.form-field' );
2045 }
2046
2047 function handleAjaxLoadFieldSuccess( html, $thisField, field ) {
2048 let key, $nextSet;
2049
2050 html = html.replace( /^\s+|\s+$/g, '' );
2051 if ( html.indexOf( '{' ) !== 0 ) {
2052 jQuery( '.frm_load_now' ).removeClass( '.frm_load_now' ).html( 'Error' );
2053 return;
2054 }
2055
2056 html = JSON.parse( html );
2057 for ( key in html ) {
2058 jQuery( '#frm_field_id_' + key ).replaceWith( html[key]);
2059 setupSortable( '#frm_field_id_' + key + '.edit_field_type_divider ul.frm_sorting' );
2060 makeDraggable( document.getElementById( 'frm_field_id_' + key ) );
2061 }
2062
2063 $nextSet = $thisField.nextAll( '.frm_field_loading:not(.frm_load_now)' );
2064 if ( $nextSet.length ) {
2065 loadFields( $nextSet.attr( 'id' ) );
2066 } else {
2067 // go up a level
2068 $nextSet = jQuery( document.getElementById( 'frm-show-fields' ) ).find( '.frm_field_loading:not(.frm_load_now)' );
2069 if ( $nextSet.length ) {
2070 loadFields( $nextSet.attr( 'id' ) );
2071 }
2072 }
2073
2074 initiateMultiselect();
2075 renumberPageBreaks();
2076 maybeHideQuantityProductFieldOption();
2077
2078 const loadedEvent = new Event( 'frm_ajax_loaded_field', { bubbles: false });
2079 loadedEvent.frmFields = field.map( f => JSON.parse( f ) );
2080 document.dispatchEvent( loadedEvent );
2081 }
2082
2083 function addFieldClick() {
2084 /*jshint validthis:true */
2085 const $thisObj = jQuery( this );
2086 // there is no real way to disable a <a> (with a valid href attribute) in HTML - https://css-tricks.com/how-to-disable-links/
2087 if ( $thisObj.hasClass( 'disabled' ) ) {
2088 return false;
2089 }
2090
2091 const $button = $thisObj.closest( '.frmbutton' );
2092 const fieldType = $button.attr( 'id' );
2093
2094 if ( shouldStopInsertingField( fieldType ) ) {
2095 return;
2096 }
2097
2098 let hasBreak = 0;
2099 if ( 'summary' === fieldType ) {
2100 hasBreak = $newFields.children( 'li[data-type="break"]' ).length > 0 ? 1 : 0;
2101 }
2102
2103 const formId = thisFormId;
2104 jQuery.ajax({
2105 type: 'POST',
2106 url: ajaxurl,
2107 data: getInsertNewFieldArgs( fieldType, 0, formId, hasBreak ),
2108 success: function( msg ) {
2109 handleAddFieldClickResponse( msg );
2110
2111 const fieldId = checkMsgForFieldId( msg );
2112 if ( fieldId ) {
2113 /**
2114 * Fires after a field is added.
2115 *
2116 * @since 6.23
2117 *
2118 * @param {Object} fieldData The field data.
2119 * @param {String} fieldData.field The field HTML.
2120 * @param {String} fieldData.field_type The field type.
2121 * @param {String} fieldData.form_id The form ID.
2122 */
2123 wp.hooks.doAction( 'frm_after_field_added_in_form_builder', {
2124 field: msg,
2125 fieldId,
2126 fieldType,
2127 form_id: formId,
2128 });
2129 }
2130 },
2131 error: handleInsertFieldError
2132 });
2133 return false;
2134 }
2135
2136 function handleAddFieldClickResponse( msg ) {
2137 document.getElementById( 'frm_form_editor_container' ).classList.add( 'frm-has-fields' );
2138 const replaceWith = wrapFieldLi( msg );
2139 const submitField = $newFields[0].querySelector( '.edit_field_type_submit' );
2140
2141 if ( ! submitField ) {
2142 $newFields.append( replaceWith );
2143 } else {
2144 jQuery( submitField.closest( '.frm_field_box:not(.form-field)' ) ).before( replaceWith );
2145 }
2146
2147 afterAddField( msg, true );
2148
2149 replaceWith.each(
2150 function() {
2151 makeDroppable( this.querySelector( 'ul.frm_sorting' ) );
2152 makeDraggable( this.querySelector( '.form-field' ), '.frm-move' );
2153 }
2154 );
2155 }
2156
2157 function insertFormField( fieldType, fieldOptions = {} ) {
2158
2159 return new Promise( ( resolve ) => {
2160 const formId = thisFormId;
2161 let hasBreak = 0;
2162
2163 if ( 'summary' === fieldType ) {
2164 hasBreak = $newFields.children( 'li[data-type="break"]' ).length > 0 ? 1 : 0;
2165 }
2166
2167 jQuery.ajax({
2168 type: 'POST',
2169 url: ajaxurl,
2170 data: Object.assign( getInsertNewFieldArgs( fieldType, 0, formId, hasBreak ), { field_options: fieldOptions } ),
2171 success: function( msg ) {
2172 resolve( msg );
2173
2174 setTimeout( () => {
2175 updateFieldOrder();
2176 afterAddField( msg, true );
2177
2178 const fieldId = checkMsgForFieldId( msg );
2179 if ( fieldId ) {
2180 /**
2181 * Fires after a field is added.
2182 *
2183 * @since 6.23
2184 *
2185 * @param {Object} fieldData The field data.
2186 * @param {String} fieldData.field The field HTML.
2187 * @param {String} fieldData.field_type The field type.
2188 * @param {String} fieldData.form_id The form ID.
2189 */
2190 wp.hooks.doAction( 'frm_after_field_added_in_form_builder', {
2191 field: msg,
2192 fieldId,
2193 fieldType,
2194 form_id: formId,
2195 });
2196 }
2197 }, 10 );
2198 },
2199 error: handleInsertFieldError
2200 });
2201 } );
2202 }
2203
2204 function maybeHideQuantityProductFieldOption() {
2205 let hide = true,
2206 opts = document.querySelectorAll( '.frmjs_prod_field_opt_cont' );
2207
2208 if ( $newFields.find( 'li.edit_field_type_product' ).length > 1 ) {
2209 hide = false;
2210 }
2211
2212 for ( let i = 0; i < opts.length; i++ ) {
2213 if ( hide ) {
2214 opts[ i ].classList.add( 'frm_hidden' );
2215 } else {
2216 opts[ i ].classList.remove( 'frm_hidden' );
2217 }
2218 }
2219 }
2220
2221 /**
2222 * Returns true if a field can be duplicated.
2223 *
2224 * @since 6.19
2225 *
2226 * @param {HTMLElement} field
2227 * @param {number} maxFieldsInGroup
2228 *
2229 * @returns {Boolean}
2230 */
2231 function canDuplicateField( field, maxFieldsInGroup ) {
2232 if ( field.classList.contains( 'frm-page-collapsed' ) ) {
2233 return false;
2234 }
2235 const fieldGroup = field.closest( 'li.frm_field_box:not(.form-field)' );
2236 if ( ! fieldGroup ) {
2237 return true;
2238 }
2239 const fieldsInGroup = getFieldsInRow( jQuery( fieldGroup.querySelector( 'ul' ) ) ).length;
2240 return fieldsInGroup < maxFieldsInGroup;
2241 }
2242
2243 function duplicateField() {
2244 let $field, fieldId, children, newRowId, fieldOrder;
2245 const maxFieldsInGroup = 6;
2246
2247 $field = jQuery( this ).closest( 'li.form-field' );
2248 newRowId = this.getAttribute( 'frm-target-row-id' );
2249
2250 if ( ! ( newRowId && newRowId.startsWith( 'frm_field_group_' ) ) && ! canDuplicateField( $field.get( 0 ), maxFieldsInGroup ) ) {
2251 /* translators: %1$d: Maximum number of fields allowed in a field group. */
2252 infoModal( sprintf( __( 'You can only have a maximum of %1$d fields in a field group. Delete or move out a field from the group and try again.', 'formidable' ), maxFieldsInGroup ) );
2253 return;
2254 }
2255
2256 closeOpenFieldDropdowns();
2257 fieldId = $field.data( 'fid' );
2258 children = fieldsInSection( fieldId );
2259
2260 if ( null !== newRowId ) {
2261 fieldOrder = this.getAttribute( 'frm-field-order' );
2262 }
2263
2264 jQuery.ajax({
2265 type: 'POST',
2266 url: ajaxurl,
2267 data: {
2268 action: 'frm_duplicate_field',
2269 field_id: fieldId,
2270 form_id: thisFormId,
2271 children: children,
2272 nonce: frmGlobal.nonce
2273 },
2274 success: function( msg ) {
2275 let newRow;
2276
2277 let replaceWith;
2278
2279 if ( null !== newRowId ) {
2280 newRow = document.getElementById( newRowId );
2281 if ( null !== newRow ) {
2282 replaceWith = msgAsjQueryObject( msg );
2283 jQuery( newRow ).append( replaceWith );
2284 makeDraggable( replaceWith.get( 0 ), '.frm-move' );
2285 if ( null !== fieldOrder ) {
2286 newRow.lastElementChild.setAttribute( 'frm-field-order', fieldOrder );
2287 }
2288 jQuery( newRow ).trigger(
2289 'frm_added_duplicated_field_to_row',
2290 {
2291 duplicatedFieldHtml: msg,
2292 originalFieldId: fieldId
2293 }
2294 );
2295 afterAddField( msg, false );
2296 setLayoutClassesForDuplicatedFieldInGroup( $field.get( 0 ), replaceWith.get( 0 ) );
2297 return;
2298 }
2299 }
2300
2301 if ( $field.siblings( 'li.form-field' ).length ) {
2302 replaceWith = msgAsjQueryObject( msg );
2303 $field.after( replaceWith );
2304 syncLayoutClasses( $field );
2305 makeDraggable( replaceWith.get( 0 ), '.frm-move' );
2306 } else {
2307 replaceWith = wrapFieldLi( msg );
2308 $field.parent().parent().after( replaceWith );
2309 makeDroppable( replaceWith.get( 0 ).querySelector( 'ul.frm_sorting' ) );
2310 makeDraggable( replaceWith.get( 0 ).querySelector( 'li.form-field' ), '.frm-move' );
2311 }
2312
2313 updateFieldOrder();
2314 afterAddField( msg, false );
2315 maybeDuplicateUnsavedSettings( fieldId, msg );
2316 toggleOneSectionHolder( replaceWith.find( '.start_divider' ) );
2317 $field[0].querySelector( '.frm-dropdown-menu.dropdown-menu-right' )?.classList.remove( 'show' );
2318 setLayoutClassesForDuplicatedFieldInGroup( $field.get( 0 ), replaceWith.get( 0 ) );
2319 }
2320 });
2321 return false;
2322 }
2323
2324 /**
2325 * Sets the layout classes for a duplicated field in a field group from the layout classes of the original field.
2326 *
2327 * @param {HTMLElement} field The original field.
2328 * @param {HTMLElement} newField The duplicated field.
2329 *
2330 * @returns {void}
2331 */
2332 function setLayoutClassesForDuplicatedFieldInGroup( field, newField ) {
2333 const hoverTarget = field.closest( '.frm-field-group-hover-target' );
2334 if ( ! hoverTarget || ! isFieldGroup( hoverTarget.parentElement ) ) {
2335 return;
2336 }
2337 const fieldId = field.dataset.fid;
2338 let fieldClasses = document.getElementById( 'frm_classes_' + fieldId )?.value;
2339 if ( ! fieldClasses ) {
2340 return;
2341 }
2342 fieldClasses = fieldClasses.replace( 'frm_first', '' );
2343 if ( ! newField.className.includes( fieldClasses ) ) {
2344 newField.className += ' ' + fieldClasses;
2345
2346 const classesInput = document.getElementById( 'frm_classes_' + newField.dataset.fid );
2347 if ( classesInput ) {
2348 classesInput.value = fieldClasses;
2349 }
2350 }
2351 }
2352
2353 function maybeDuplicateUnsavedSettings( originalFieldId, newFieldHtml ) {
2354 let originalSettings, newFieldId, copySettings, fieldOptionKeys, originalDefault, copyDefault;
2355
2356 originalSettings = document.getElementById( 'frm-single-settings-' + originalFieldId );
2357 if ( null === originalSettings ) {
2358 return;
2359 }
2360
2361 newFieldId = jQuery( newFieldHtml ).attr( 'data-fid' );
2362 if ( 'undefined' === typeof newFieldId ) {
2363 return;
2364 }
2365
2366 copySettings = document.getElementById( 'frm-single-settings-' + newFieldId );
2367 if ( null === copySettings ) {
2368 return;
2369 }
2370
2371 fieldOptionKeys = [
2372 'name', 'required', 'unique', 'read_only', 'placeholder', 'description', 'size', 'max', 'format', 'prepend', 'append', 'separate_value'
2373 ];
2374
2375 originalSettings.querySelectorAll( 'input[name^="field_options["], textarea[name^="field_options["]' ).forEach(
2376 function( originalSetting ) {
2377 let key, tagType, copySetting;
2378
2379 key = getKeyFromSettingInput( originalSetting );
2380
2381 if ( 'options' === key ) {
2382 copyOption( originalSetting, copySettings, originalFieldId, newFieldId );
2383 return;
2384 }
2385
2386 if ( -1 === fieldOptionKeys.indexOf( key ) ) {
2387 return;
2388 }
2389
2390 tagType = originalSetting.matches( 'input' ) ? 'input' : 'textarea';
2391 copySetting = copySettings.querySelector( tagType + '[name="field_options[' + key + '_' + newFieldId + ']"]' );
2392 if ( null === copySetting ) {
2393 return;
2394 }
2395
2396 if ( 'checkbox' === originalSetting.type ) {
2397 if ( originalSetting.checked !== copySetting.checked ) {
2398 jQuery( copySetting ).trigger( 'click' );
2399 }
2400 } else if ( 'text' === originalSetting.type || 'textarea' === tagType ) {
2401 if ( originalSetting.value !== copySetting.value ) {
2402 copySetting.value = originalSetting.value;
2403 jQuery( copySetting ).trigger( 'change' );
2404 }
2405 }
2406 }
2407 );
2408
2409 originalDefault = originalSettings.querySelector( 'input[name="default_value_' + originalFieldId + '"]' );
2410 if ( null !== originalDefault ) {
2411 copyDefault = copySettings.querySelector( 'input[name="default_value_' + newFieldId + '"]' );
2412 if ( null !== copyDefault && originalDefault.value !== copyDefault.value ) {
2413 copyDefault.value = originalDefault.value;
2414 jQuery( copyDefault ).trigger( 'change' );
2415 }
2416 }
2417 }
2418
2419 function copyOption( originalSetting, copySettings, originalFieldId, newFieldId ) {
2420 let remainingKeyDetails, copyKey, copySetting;
2421 remainingKeyDetails = originalSetting.name.substr( 23 + ( '' + originalFieldId ).length );
2422 copyKey = 'field_options[options_' + newFieldId + ']' + remainingKeyDetails;
2423 copySetting = copySettings.querySelector( 'input[name="' + copyKey + '"]' );
2424 if ( null !== copySetting && copySetting.value !== originalSetting.value ) {
2425 copySetting.value = originalSetting.value;
2426 jQuery( copySetting ).trigger( 'change' );
2427 }
2428 }
2429
2430 function getKeyFromSettingInput( input ) {
2431 let nameWithoutPrefix, nameSplit;
2432 nameWithoutPrefix = input.name.substr( 14 );
2433 nameSplit = nameWithoutPrefix.split( '_' );
2434 nameSplit.pop();
2435 return nameSplit.join( '_' );
2436 }
2437
2438 function closeOpenFieldDropdowns() {
2439 const openSettings = document.querySelector( '.frm-field-settings-open' );
2440 if ( null !== openSettings ) {
2441 openSettings.classList.remove( 'frm-field-settings-open' );
2442 jQuery( document ).off( 'click', '#frm_builder_page', handleClickOutsideOfFieldSettings );
2443 jQuery( '.frm-field-action-icons .dropdown.open' ).removeClass( 'open' );
2444 }
2445 }
2446
2447 function handleClickOutsideOfFieldSettings( event ) {
2448 if ( ! jQuery( event.originalEvent.target ).closest( '.frm-field-action-icons' ).length ) {
2449 closeOpenFieldDropdowns();
2450 }
2451 }
2452
2453 function checkForMultiselectKeysOnMouseMove( event ) {
2454 const keyIsDown = ! ! ( event.ctrlKey || event.metaKey || event.shiftKey );
2455 jQuery( builderPage ).toggleClass( 'frm-multiselect-key-is-down', keyIsDown );
2456 checkForActiveHoverTarget( event );
2457 }
2458
2459 function checkForActiveHoverTarget( event ) {
2460 let container, elementFromPoint, list, previousHoverTarget;
2461
2462 container = postBodyContent;
2463 if ( container.classList.contains( 'frm-dragging-field' ) ) {
2464 return;
2465 }
2466
2467 if ( null !== document.querySelector( '.frm-field-group-hover-target .frm-field-settings-open' ) ) {
2468 // do not set a hover target if a dropdown is open for the current hover target.
2469 return;
2470 }
2471
2472 elementFromPoint = document.elementFromPoint( event.clientX, event.clientY );
2473 if ( null !== elementFromPoint && ! elementFromPoint.classList.contains( 'edit_field_type_divider' ) ) {
2474
2475 list = elementFromPoint.closest( 'ul.frm_sorting' );
2476
2477 if ( null !== list && ! list.classList.contains( 'start_divider' ) && 'frm-show-fields' !== list.id ) {
2478 previousHoverTarget = maybeRemoveGroupHoverTarget();
2479 if ( false !== previousHoverTarget && ! jQuery( previousHoverTarget ).is( list ) ) {
2480 destroyFieldGroupPopup();
2481 }
2482 updateFieldGroupControls( jQuery( list ), getFieldsInRow( jQuery( list ) ).length );
2483 list.classList.add( 'frm-field-group-hover-target' );
2484 jQuery( '#wpbody-content' ).on( 'mousemove', maybeRemoveHoverTargetOnMouseMove );
2485 }
2486 }
2487 }
2488
2489 function maybeRemoveGroupHoverTarget() {
2490 let controls, previousHoverTarget;
2491
2492 controls = document.getElementById( 'frm_field_group_controls' );
2493 if ( null !== controls ) {
2494 controls.style.display = 'none';
2495 }
2496
2497 previousHoverTarget = document.querySelector( '.frm-field-group-hover-target' );
2498 if ( null === previousHoverTarget ) {
2499 return false;
2500 }
2501
2502 jQuery( '#wpbody-content' ).off( 'mousemove', maybeRemoveHoverTargetOnMouseMove );
2503 previousHoverTarget.classList.remove( 'frm-field-group-hover-target' );
2504 return previousHoverTarget;
2505 }
2506
2507 function maybeRemoveHoverTargetOnMouseMove( event ) {
2508 const elementFromPoint = document.elementFromPoint( event.clientX, event.clientY );
2509 if ( null !== elementFromPoint && null !== elementFromPoint.closest( '#frm-show-fields' ) ) {
2510 return;
2511 }
2512 maybeRemoveGroupHoverTarget();
2513 }
2514
2515 function onFieldActionDropdownShow( isFieldGroup ) {
2516 unselectFieldGroups();
2517 // maybe offset the dropdown if it goes off of the right of the screen.
2518 setTimeout(
2519 function() {
2520 let ul, $ul;
2521 ul = document.querySelector( '.dropdown.show .frm-dropdown-menu' );
2522 if ( null === ul ) {
2523 return;
2524 }
2525 if ( null === ul.getAttribute( 'aria-label' ) ) {
2526 ul.setAttribute( 'aria-label', __( 'More Options', 'formidable' ) );
2527 }
2528 if ( 0 === ul.children.length ) {
2529 fillFieldActionDropdown( ul, true === isFieldGroup );
2530 }
2531 $ul = jQuery( ul );
2532 if ( $ul.offset().left > jQuery( window ).width() - $ul.outerWidth() ) {
2533 ul.style.left = ( -$ul.outerWidth() ) + 'px';
2534 }
2535 const firstAnchor = ul.firstElementChild.querySelector( 'a' );
2536 if ( firstAnchor ) {
2537 firstAnchor.focus();
2538 }
2539 },
2540 0
2541 );
2542 }
2543
2544 function onFieldGroupActionDropdownShow() {
2545 onFieldActionDropdownShow( true );
2546 }
2547
2548 function changeSectionStyle( e ) {
2549 const collapsedSection = e.target.closest( '.frm-section-collapsed' );
2550 if ( ! collapsedSection ) {
2551 return;
2552 }
2553
2554 if ( e.type === 'show' ) {
2555 collapsedSection.style.zIndex = 3;
2556 } else {
2557 collapsedSection.style.zIndex = 1;
2558 }
2559 }
2560
2561 function fillFieldActionDropdown( ul, isFieldGroup ) {
2562 let classSuffix, options;
2563 classSuffix = isFieldGroup ? '_field_group' : '_field';
2564 options = [ getDeleteActionOption( isFieldGroup ), getDuplicateActionOption( isFieldGroup ) ];
2565 if ( ! isFieldGroup ) {
2566 options.push(
2567 { class: 'frm_select', icon: 'frm_settings_icon', label: __( 'Field Settings', 'formidable' ) }
2568 );
2569 }
2570 options.forEach(
2571 function( option ) {
2572 let li, anchor, span;
2573 li = document.createElement( 'div' );
2574 li.classList.add( 'frm_more_options_li', 'dropdown-item' );
2575
2576 anchor = document.createElement( 'a' );
2577 anchor.classList.add( option.class + classSuffix );
2578 anchor.setAttribute( 'href', '#' );
2579 makeTabbable( anchor );
2580
2581 span = document.createElement( 'span' );
2582 span.textContent = option.label;
2583 anchor.innerHTML = '<svg class="frmsvg"><use xlink:href="#' + option.icon + '"></use></svg>';
2584 anchor.appendChild( document.createTextNode( ' ' ) );
2585 anchor.appendChild( span );
2586
2587 li.appendChild( anchor );
2588 ul.appendChild( li );
2589 }
2590 );
2591 }
2592
2593 function getDeleteActionOption( isFieldGroup ) {
2594 const option = { class: 'frm_delete', icon: 'frm_delete_icon' };
2595 option.label = isFieldGroup ? __( 'Delete Group', 'formidable' ) : __( 'Delete', 'formidable' );
2596 return option;
2597 }
2598
2599 function getDuplicateActionOption( isFieldGroup ) {
2600 const option = { class: 'frm_clone', icon: 'frm_clone_icon' };
2601 option.label = isFieldGroup ? __( 'Duplicate Group', 'formidable' ) : __( 'Duplicate', 'formidable' );
2602 return option;
2603 }
2604
2605 function wrapFieldLi( field ) {
2606 const wrapper = div();
2607
2608 if ( 'string' === typeof field ) {
2609 wrapper.innerHTML = field;
2610 } else {
2611 wrapper.appendChild( field );
2612 }
2613
2614 let result = jQuery();
2615 Array.from( wrapper.children ).forEach(
2616 li => {
2617 result = result.add(
2618 jQuery( '<li>' )
2619 .addClass( 'frm_field_box' )
2620 .html(
2621 jQuery( '<ul>' ).addClass( 'frm_grid_container frm_sorting' ).append( li )
2622 )
2623 );
2624 }
2625 );
2626
2627 return result;
2628 }
2629
2630 function wrapFieldLiInPlace( li ) {
2631 const ul = tag(
2632 'ul',
2633 {
2634 className: 'frm_grid_container frm_sorting'
2635 }
2636 );
2637 const wrapper = tag(
2638 'li',
2639 {
2640 className: 'frm_field_box',
2641 child: ul
2642 }
2643 );
2644
2645 li.replaceWith( wrapper );
2646 ul.appendChild( li );
2647
2648 makeDroppable( ul );
2649 makeDraggable( wrapper, '.frm-move' );
2650 }
2651
2652 function afterAddField( msg, addFocus ) {
2653 const regex = /id="(\S+)"/;
2654 const match = regex.exec( msg );
2655 const field = document.getElementById( match[1]);
2656 const section = '#' + match[1] + '.edit_field_type_divider ul.frm_sorting.start_divider';
2657 const $thisSection = jQuery( section );
2658 const type = field.getAttribute( 'data-type' );
2659
2660 checkHtmlForNewFields( msg );
2661
2662 let toggled = false;
2663
2664 fieldUpdated();
2665 setupSortable( section );
2666
2667 if ( 'quantity' === type ) {
2668 // try to automatically attach a product field
2669 maybeSetProductField( field );
2670 }
2671
2672 if ( 'product' === type || 'quantity' === type ) {
2673 // quantity too needs to be a part of the if stmt especially cos of the very
2674 // 1st quantity field (or even if it's just one quantity field in the form).
2675 maybeHideQuantityProductFieldOption();
2676 }
2677
2678 if ( $thisSection.length ) {
2679 $thisSection.parent( '.frm_field_box' ).children( '.frm_no_section_fields' ).addClass( 'frm_block' );
2680 } else {
2681 const $parentSection = jQuery( field ).closest( 'ul.frm_sorting.start_divider' );
2682 if ( $parentSection.length ) {
2683 toggleOneSectionHolder( $parentSection );
2684 toggled = true;
2685 }
2686 }
2687
2688 if ( msg.indexOf( 'frm-collapse-page' ) !== -1 ) {
2689 renumberPageBreaks();
2690 }
2691
2692 addClass( field, 'frm-newly-added' );
2693 setTimeout( function() {
2694 field.classList.remove( 'frm-newly-added' );
2695 }, 1000 );
2696
2697 if ( addFocus ) {
2698 const bounding = field.getBoundingClientRect(),
2699 container = document.getElementById( 'post-body-content' ),
2700 inView = ( bounding.top >= 0 &&
2701 bounding.left >= 0 &&
2702 bounding.right <= ( window.innerWidth || document.documentElement.clientWidth ) &&
2703 bounding.bottom <= ( window.innerHeight || document.documentElement.clientHeight )
2704 );
2705
2706 if ( ! inView ) {
2707 container.scroll({
2708 top: container.scrollHeight,
2709 left: 0,
2710 behavior: 'smooth'
2711 });
2712 }
2713
2714 if ( toggled === false ) {
2715 toggleOneSectionHolder( $thisSection );
2716 }
2717 }
2718
2719 deselectFields();
2720 initiateMultiselect();
2721
2722 document.getElementById( 'frm-show-fields' ).classList.remove( 'frm-over-droppable' );
2723
2724 const addedEvent = new Event( 'frm_added_field', { bubbles: false });
2725 addedEvent.frmField = field;
2726 addedEvent.frmSection = section;
2727 addedEvent.frmType = type;
2728 addedEvent.frmToggles = toggled;
2729 document.dispatchEvent( addedEvent );
2730 }
2731
2732 /**
2733 * Since multiple new fields may get added when a new field is inserted, check the HTML.
2734 *
2735 * @param {string} html
2736 * @returns {void}
2737 */
2738 function checkHtmlForNewFields( html ) {
2739 const element = div();
2740 element.innerHTML = html;
2741 element.querySelectorAll( '.form-field' ).forEach( addFieldIdToDraftFieldsInput );
2742 }
2743
2744 /**
2745 * @param {HTMLElement} field
2746 * @returns {void}
2747 */
2748 function addFieldIdToDraftFieldsInput( field ) {
2749 if ( ! field.dataset.fid ) {
2750 return;
2751 }
2752
2753 const draftInput = document.getElementById( 'draft_fields' );
2754 if ( ! draftInput ) {
2755 return;
2756 }
2757
2758 if ( '' === draftInput.value ) {
2759 draftInput.value = field.dataset.fid;
2760 } else {
2761 const split = draftInput.value.split( ',' );
2762 if ( ! split.includes( field.dataset.fid ) ) {
2763 draftInput.value += ',' + field.dataset.fid;
2764 }
2765 }
2766 }
2767
2768 function clearSettingsBox( preventFieldGroups ) {
2769 jQuery( '#new_fields .frm-single-settings' ).addClass( 'frm_hidden' );
2770 jQuery( '#frm-options-panel > .frm-single-settings' ).removeClass( 'frm_hidden' );
2771 deselectFields( preventFieldGroups );
2772 }
2773
2774 function deselectFields( preventFieldGroups ) {
2775 jQuery( 'li.ui-state-default.selected' ).removeClass( 'selected' );
2776 jQuery( '.frm-show-field-settings.selected' ).removeClass( 'selected' );
2777 if ( ! preventFieldGroups ) {
2778 unselectFieldGroups();
2779 }
2780 }
2781
2782 function scrollToField( field ) {
2783 const newPos = field.getBoundingClientRect().top,
2784 container = document.getElementById( 'post-body-content' );
2785
2786 if ( typeof animate === 'undefined' ) {
2787 jQuery( container ).scrollTop( newPos );
2788 } else {
2789 // TODO: smooth scroll
2790 jQuery( container ).animate({ scrollTop: newPos }, 500 );
2791 }
2792 }
2793
2794 function checkCalculationCreatedByUser() {
2795 const calculation = this.value;
2796 let warningMessage = checkMatchingParens( calculation );
2797 warningMessage += checkShortcodes( calculation, this );
2798
2799 if ( warningMessage !== '' ) {
2800 infoModal( calculation + '\n\n' + warningMessage );
2801 }
2802 }
2803
2804 /**
2805 * Checks a string for parens, brackets, and curly braces and returns a message if any unmatched are found.
2806 * @param formula
2807 * @returns {string}
2808 */
2809 function checkMatchingParens( formula ) {
2810
2811 let stack = [],
2812 formulaArray = formula.split( '' ),
2813 length = formulaArray.length,
2814 opening = [ '{', '[', '(' ],
2815 closing = {
2816 '}': '{',
2817 ')': '(',
2818 ']': '['
2819 },
2820 unmatchedClosing = [],
2821 msg = '',
2822 i, top;
2823
2824 for ( i = 0; i < length; i++ ) {
2825 if ( opening.includes( formulaArray[i]) ) {
2826 stack.push( formulaArray[i]);
2827 continue;
2828 }
2829 if ( closing.hasOwnProperty( formulaArray[i]) ) {
2830 top = stack.pop();
2831 if ( top !== closing[formulaArray[i]]) {
2832 unmatchedClosing.push( formulaArray[i]);
2833 }
2834 }
2835 }
2836
2837 if ( stack.length > 0 || unmatchedClosing.length > 0 ) {
2838 msg = frmAdminJs.unmatched_parens + '\n\n';
2839 return msg;
2840 }
2841
2842 return '';
2843 }
2844
2845 /**
2846 * Checks a calculation for shortcodes that shouldn't be in it and returns a message if found.
2847 * @param calculation
2848 * @param inputElement
2849 * @returns {string}
2850 */
2851 function checkShortcodes( calculation, inputElement ) {
2852 let msg = checkNonNumericShortcodes( calculation, inputElement );
2853 msg += checkNonFormShortcodes( calculation );
2854
2855 return msg;
2856 }
2857
2858 /**
2859 * Checks if a numeric calculation has shortcodes that output non-numeric strings and returns a message if found.
2860 * @param calculation
2861 *
2862 * @param inputElement
2863 * @returns {string}
2864 */
2865 function checkNonNumericShortcodes( calculation, inputElement ) {
2866
2867 let msg = '';
2868
2869 if ( isTextCalculation( inputElement ) ) {
2870 return msg;
2871 }
2872
2873 const nonNumericShortcodes = getNonNumericShortcodes();
2874
2875 if ( nonNumericShortcodes.test( calculation ) ) {
2876 msg = frmAdminJs.text_shortcodes + '\n\n';
2877 }
2878
2879 return msg;
2880 }
2881
2882 /**
2883 * Determines if the calculation input is from a text calculation.
2884 *
2885 * @param inputElement
2886 */
2887 function isTextCalculation( inputElement ) {
2888 return jQuery( inputElement ).siblings( 'label[for^="calc_type"]' ).children( 'input' ).prop( 'checked' );
2889 }
2890
2891 /**
2892 * Returns a regular expression of shortcodes that can't be used in numeric calculations.
2893 * @returns {RegExp}
2894 */
2895 function getNonNumericShortcodes() {
2896 return /\[(date|time|email|ip)\]/;
2897 }
2898
2899 /**
2900 * Checks if a string has any shortcodes that do not belong in forms and returns a message if any are found.
2901 * @param formula
2902 * @returns {string}
2903 */
2904 function checkNonFormShortcodes( formula ) {
2905 let nonFormShortcodes = getNonFormShortcodes(),
2906 msg = '';
2907
2908 if ( nonFormShortcodes.test( formula ) ) {
2909 msg += frmAdminJs.view_shortcodes + '\n\n';
2910 }
2911
2912 return msg;
2913 }
2914
2915 /**
2916 * Returns a regular expression of shortcodes that can't be used in forms but can be used in Views, Email
2917 * Notifications, and other Formidable areas.
2918 *
2919 * @returns {RegExp}
2920 */
2921 function getNonFormShortcodes() {
2922 return /\[id\]|\[key\]|\[if\s\w+\]|\[foreach\s\w+\]|\[created-at(\s*)?/g;
2923 }
2924
2925 function isCalcBoxType( box, listClass ) {
2926 const list = jQuery( box ).find( '.frm_code_list' );
2927 return 1 === list.length && list.hasClass( listClass );
2928 }
2929
2930 function extractExcludedOptions( exclude ) {
2931 const opts = [];
2932 if ( ! Array.isArray( exclude ) ) {
2933 return opts;
2934 }
2935
2936 for ( let i = 0; i < exclude.length; i++ ) {
2937 if ( exclude[ i ].startsWith( '[' ) ) {
2938 opts.push( exclude[ i ]);
2939 // remove it
2940 exclude.splice( i, 1 );
2941 // https://love2dev.com/blog/javascript-remove-from-array/#remove-from-array-splice-value
2942 i--;
2943 }
2944 }
2945
2946 return opts;
2947 }
2948
2949 function hasExcludedOption( field, excludedOpts ) {
2950 let hasOption = false;
2951 for ( let i = 0; i < excludedOpts.length; i++ ) {
2952 const inputs = document.getElementsByName( getFieldOptionInputName( excludedOpts[ i ], field.fieldId ) );
2953 // 2nd condition checks that there's at least one non-empty value
2954 if ( inputs.length && jQuery( inputs[0]).val() ) {
2955 hasOption = true;
2956 break;
2957 }
2958 }
2959 return hasOption;
2960 }
2961
2962 function getFieldOptionInputName( opt, fieldId ) {
2963 const at = opt.indexOf( ']' );
2964 return 'field_options' + opt.substring( 0, at ) + '_' + fieldId + opt.substring( at );
2965 }
2966
2967 function popCalcFields( v, force ) {
2968 let box, exclude, fields, i, list,
2969 p = jQuery( v ).closest( '.frm-single-settings' ),
2970 calc = p.find( '.frm-calc-field' );
2971
2972 if ( ! force && ( ! calc.length || calc.val() === '' || calc.is( ':hidden' ) ) ) {
2973 return;
2974 }
2975
2976 const isSummary = isCalcBoxType( v, 'frm_js_summary_list' );
2977
2978 const fieldId = p.find( 'input[name="frm_fields_submitted[]"]' ).val();
2979
2980 if ( force ) {
2981 box = v;
2982 } else {
2983 box = document.getElementById( 'frm-calc-box-' + fieldId );
2984 }
2985
2986 exclude = getExcludeArray( box, isSummary );
2987 const excludedOpts = extractExcludedOptions( exclude );
2988
2989 fields = getFieldList();
2990 list = document.getElementById( 'frm-calc-list-' + fieldId );
2991 list.innerHTML = '';
2992
2993 for ( i = 0; i < fields.length; i++ ) {
2994 if ( ( exclude && exclude.includes( fields[ i ].fieldType ) ) ||
2995 ( excludedOpts.length && hasExcludedOption( fields[ i ], excludedOpts ) ) ) {
2996 continue;
2997 }
2998
2999 const span = document.createElement( 'span' );
3000 span.appendChild( document.createTextNode( '[' + fields[i].fieldId + ']' ) );
3001
3002 const a = document.createElement( 'a' );
3003 a.setAttribute( 'href', '#' );
3004 a.setAttribute( 'data-code', fields[i].fieldId );
3005 a.classList.add( 'frm_insert_code' );
3006 a.appendChild( span );
3007 a.appendChild( document.createTextNode( fields[i].fieldName ) );
3008
3009 const li = document.createElement( 'li' );
3010 li.classList.add( 'frm-field-list-' + fieldId );
3011 li.classList.add( 'frm-field-list-' + fields[i].fieldType );
3012 li.appendChild( a );
3013 list.appendChild( li );
3014 }
3015 }
3016
3017 function getExcludeArray( calcBox, isSummary ) {
3018 const exclude = JSON.parse( calcBox.getElementsByClassName( 'frm_code_list' )[0].getAttribute( 'data-exclude' ) );
3019
3020 if ( isSummary ) {
3021 // includedExtras are those that are normally excluded from the summary but the form owner can choose to include,
3022 // when they have been chosen to be included, then they can now be manually excluded in the calc box.
3023 const includedExtras = getIncludedExtras();
3024 if ( includedExtras.length ) {
3025 for ( let i = 0; i < exclude.length; i++ ) {
3026 if ( includedExtras.includes( exclude[ i ]) ) {
3027 // remove it
3028 exclude.splice( i, 1 );
3029 // https://love2dev.com/blog/javascript-remove-from-array/#remove-from-array-splice-value
3030 i--;
3031 }
3032 }
3033 }
3034 }
3035
3036 return exclude;
3037 }
3038
3039 function getIncludedExtras() {
3040 const checked = [];
3041 const checkboxes = document.getElementsByClassName( 'frm_include_extras_field' );
3042
3043 for ( let i = 0; i < checkboxes.length; i++ ) {
3044 if ( checkboxes[i].checked ) {
3045 checked.push( checkboxes[i].value );
3046 }
3047 }
3048
3049 return checked;
3050 }
3051
3052 function rePopCalcFieldsForSummary() {
3053 popCalcFields( jQuery( '.frm-inline-modal.postbox:has(.frm_js_summary_list)' )[0], true );
3054 }
3055
3056 function getFieldList( fieldType ) {
3057 let i,
3058 fields = [],
3059 allFields = document.querySelectorAll( 'li.frm_field_box' ),
3060 checkType = 'undefined' !== typeof fieldType;
3061
3062 for ( i = 0; i < allFields.length; i++ ) {
3063 // data-ftype is better (than data-type) cos of fields loaded by AJAX - which might not be ready yet
3064 if ( checkType && allFields[ i ].getAttribute( 'data-ftype' ) !== fieldType ) {
3065 continue;
3066 }
3067
3068 const fieldId = allFields[ i ].getAttribute( 'data-fid' );
3069 if ( typeof fieldId !== 'undefined' && fieldId ) {
3070 fields.push({
3071 'fieldId': fieldId,
3072 'fieldName': getPossibleValue( 'frm_name_' + fieldId ),
3073 'fieldType': getPossibleValue( 'field_options_type_' + fieldId ),
3074 'fieldKey': getPossibleValue( 'field_options_field_key_' + fieldId )
3075 });
3076 }
3077 }
3078
3079 return wp.hooks.applyFilters( 'frm_admin_get_field_list', fields, fieldType, allFields );
3080 }
3081
3082 function popProductFields( field ) {
3083 let i, checked, id,
3084 options = [],
3085 current = getCurrentProductFields( field ),
3086 fName = field.getAttribute( 'data-frmfname' ),
3087 products = getFieldList( 'product' ),
3088 quantities = getFieldList( 'quantity' ),
3089 isSelect = field.tagName === 'SELECT', // for reverse compatibility.
3090 // whether we have just 1 product and 1 quantity field & should therefore attach the latter to the former
3091 auto = 1 === quantities.length && 1 === products.length;
3092
3093 if ( isSelect ) {
3094 // This fallback can be removed after 4.05.
3095 current = field.getAttribute( 'data-frmcurrent' );
3096 }
3097
3098 for ( i = 0 ; i < products.length ; i++ ) {
3099 // let's be double sure it's string, else indexOf will fail
3100 id = products[ i ].fieldId.toString();
3101 checked = auto || -1 !== current.indexOf( id );
3102 if ( isSelect ) {
3103 // This fallback can be removed after 4.05.
3104 checked = checked ? ' selected' : '';
3105 options.push( '<option value="' + id + '"' + checked + '>' + products[ i ].fieldName + '</option>' );
3106 } else {
3107 checked = checked ? ' checked' : '';
3108 options.push( '<label class="frm6">' );
3109 options.push( '<input type="checkbox" name="' + fName + '" value="' + id + '"' + checked + '> ' + products[ i ].fieldName );
3110 options.push( '</label>' );
3111 }
3112 }
3113
3114 field.innerHTML = options.join( '' );
3115 }
3116
3117 function getCurrentProductFields( prodFieldOpt ) {
3118 const products = prodFieldOpt.querySelectorAll( '[type="checkbox"]:checked' ),
3119 idsArray = [];
3120
3121 for ( let i = 0; i < products.length; i++ ) {
3122 idsArray.push( products[ i ].value );
3123 }
3124
3125 return idsArray;
3126 }
3127
3128 function popAllProductFields() {
3129 const opts = document.querySelectorAll( '.frmjs_prod_field_opt' );
3130 for ( let i = 0; i < opts.length; i++ ) {
3131 popProductFields( opts[ i ]);
3132 }
3133 }
3134
3135 function maybeSetProductField( field ) {
3136 const fieldId = field.getAttribute( 'data-fid' ),
3137 productFieldOpt = document.getElementById( 'field_options[product_field_' + fieldId + ']' );
3138
3139 if ( null === productFieldOpt ) {
3140 return;
3141 }
3142
3143 popProductFields( productFieldOpt );
3144 // in order to move its settings to that LHS panel where
3145 // the update form resides, else it'll lose this setting
3146 moveFieldSettings( document.getElementById( 'frm-single-settings-' + fieldId ) );
3147 }
3148
3149 /**
3150 * If the element doesn't exist, use a blank value.
3151 */
3152 function getPossibleValue( id ) {
3153 const field = document.getElementById( id );
3154 if ( field !== null ) {
3155 return field.value;
3156 }
3157 return '';
3158 }
3159
3160 function liveChanges() {
3161 /*jshint validthis:true */
3162 let option,
3163 newValue = this.value,
3164 changes = document.getElementById( this.getAttribute( 'data-changeme' ) ),
3165 att = this.getAttribute( 'data-changeatt' );
3166
3167 if ( changes === null ) {
3168 return;
3169 }
3170
3171 if ( att !== null ) {
3172 if ( changes.tagName === 'SELECT' && att === 'placeholder' ) {
3173 option = changes.options[0];
3174 if ( option.value === '' ) {
3175 option.innerHTML = newValue;
3176 } else {
3177 // Create a placeholder option if there are no blank values.
3178 addBlankSelectOption( changes, newValue );
3179 }
3180 } else if ( att === 'class' ) {
3181 changeFieldClass( changes, this );
3182 } else if ( isSliderField( changes ) ) {
3183 updateSliderFieldPreview( changes, att, newValue );
3184 } else {
3185 changes.setAttribute( att, newValue );
3186 }
3187 } else if ( changes.id.indexOf( 'setup-message' ) === 0 ) {
3188 if ( newValue !== '' ) {
3189 changes.innerHTML = '<input type="text" value="" disabled />';
3190 }
3191 } else {
3192 changes.innerHTML = purifyHtml( newValue );
3193 if ( 'TEXTAREA' === changes.nodeName && changes.classList.contains( 'wp-editor-area' ) ) {
3194 // Trigger change events on wysiwyg textareas so we can also sync default values in the visual tab.
3195 jQuery( changes ).trigger( 'change' );
3196 }
3197
3198 if ( changes.classList.contains( 'frm_primary_label' ) && 'break' === changes.nextElementSibling.getAttribute( 'data-ftype' ) ) {
3199 changes.nextElementSibling.querySelector( '.frm_button_submit' ).textContent = newValue;
3200 }
3201 }
3202 }
3203
3204 function updateSliderFieldPreview( field, att, newValue ) {
3205 if ( frmGlobal.proIncludesSliderJs ) {
3206 const hookName = 'frm_update_slider_field_preview';
3207 const hookArgs = { field, att, newValue };
3208 wp.hooks.doAction( hookName, hookArgs );
3209 return;
3210 }
3211
3212 // This functionality has been moved to pro since v5.4.3. This code should be removed eventually.
3213 if ( 'value' === att ) {
3214 if ( '' === newValue ) {
3215 newValue = getSliderMidpoint( field );
3216 }
3217 field.value = newValue;
3218 } else {
3219 field.setAttribute( att, newValue );
3220 }
3221
3222 if ( -1 === [ 'value', 'min', 'max' ].indexOf( att ) ) {
3223 return;
3224 }
3225
3226 if ( ( 'max' === att || 'min' === att ) && '' === getSliderDefaultValueInput( field.id ) ) {
3227 field.value = getSliderMidpoint( field );
3228 }
3229
3230 field.parentNode.querySelector( '.frm_range_value' ).textContent = field.value;
3231 }
3232
3233 function getSliderDefaultValueInput( previewInputId ) {
3234 return document.querySelector( 'input[data-changeme="' + previewInputId + '"][data-changeatt="value"]' ).value;
3235 }
3236
3237 function getSliderMidpoint( sliderInput ) {
3238 const max = parseFloat( sliderInput.getAttribute( 'max' ) );
3239 const min = parseFloat( sliderInput.getAttribute( 'min' ) );
3240 return ( max - min ) / 2 + min;
3241 }
3242
3243 function isSliderField( previewInput ) {
3244 return 'range' === previewInput.type && previewInput.parentNode.classList.contains( 'frm_range_container' );
3245 }
3246
3247 function toggleInvalidMsg() {
3248 /*jshint validthis:true */
3249 let typeDropdown, fieldType,
3250 fieldId = this.getAttribute( 'data-fid' ),
3251 value = '';
3252
3253 [ 'field_options_max_', 'frm_format_' ].forEach( function( id ) {
3254 const input = document.getElementById( id + fieldId );
3255 if ( ! input ) {
3256 return;
3257 }
3258
3259 value += input.value;
3260 });
3261
3262 typeDropdown = document.getElementsByName( 'field_options[type_' + fieldId + ']' )[0];
3263 fieldType = typeDropdown.options[typeDropdown.selectedIndex].value;
3264
3265 if ( fieldType === 'text' ) {
3266 toggleValidationBox( '' !== value, '.frm_invalid_msg' + fieldId );
3267 }
3268 }
3269
3270 function markRequired() {
3271 /*jshint validthis:true */
3272 const thisid = this.id.replace( 'frm_', '' ),
3273 fieldId = thisid.replace( 'req_field_', '' ),
3274 checked = this.checked,
3275 label = jQuery( '#field_label_' + fieldId + ' .frm_required' );
3276
3277 toggleValidationBox( checked, '.frm_required_details' + fieldId );
3278
3279 if ( checked ) {
3280 const $reqBox = jQuery( 'input[name="field_options[required_indicator_' + fieldId + ']"]' );
3281 if ( $reqBox.val() === '' ) {
3282 $reqBox.val( '*' );
3283 }
3284 label.removeClass( 'frm_hidden' );
3285 } else {
3286 label.addClass( 'frm_hidden' );
3287 }
3288 }
3289
3290 function toggleValidationBox( hasValue, messageClass ) {
3291 $msg = jQuery( messageClass );
3292 if ( hasValue ) {
3293 $msg.fadeIn( 'fast' ).closest( '.frm_validation_msg' ).fadeIn( 'fast' );
3294 } else {
3295 //Fade out validation options
3296 const v = $msg.fadeOut( 'fast' ).closest( '.frm_validation_box' ).children( ':not(' + messageClass + '):visible' ).length;
3297 if ( v === 0 ) {
3298 $msg.closest( '.frm_validation_msg' ).fadeOut( 'fast' );
3299 }
3300 }
3301 }
3302
3303 function markUnique() {
3304 /*jshint validthis:true */
3305 const fieldId = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' );
3306 const $thisField = jQuery( '.frm_unique_details' + fieldId );
3307 if ( this.checked ) {
3308 $thisField.fadeIn( 'fast' ).closest( '.frm_validation_msg' ).fadeIn( 'fast' );
3309 $unqDetail = jQuery( '.frm_unique_details' + fieldId + ' input' );
3310 if ( $unqDetail.val() === '' ) {
3311 $unqDetail.val( frmAdminJs.default_unique );
3312 }
3313 } else {
3314 const v = $thisField.fadeOut( 'fast' ).closest( '.frm_validation_box' ).children( ':not(.frm_unique_details' + fieldId + '):visible' ).length;
3315 if ( v === 0 ) {
3316 $thisField.closest( '.frm_validation_msg' ).fadeOut( 'fast' );
3317 }
3318 }
3319 }
3320
3321 //Fade confirmation field and validation option in or out
3322 function addConf() {
3323 /*jshint validthis:true */
3324 const fieldId = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' );
3325 const val = jQuery( this ).val();
3326 const $thisField = jQuery( document.getElementById( 'frm_field_id_' + fieldId ) );
3327
3328 toggleValidationBox( val !== '', '.frm_conf_details' + fieldId );
3329
3330 if ( val !== '' ) {
3331 //Add default validation message if empty
3332 const valMsg = jQuery( '.frm_validation_box .frm_conf_details' + fieldId + ' input' );
3333 if ( valMsg.val() === '' ) {
3334 valMsg.val( frmAdminJs.default_conf );
3335 }
3336
3337 setConfirmationFieldDescriptions( fieldId );
3338
3339 //Add or remove class for confirmation field styling
3340 if ( val === 'inline' ) {
3341 $thisField.removeClass( 'frm_conf_below' ).addClass( 'frm_conf_inline' );
3342 } else if ( val === 'below' ) {
3343 $thisField.removeClass( 'frm_conf_inline' ).addClass( 'frm_conf_below' );
3344 }
3345 jQuery( '.frm-conf-box-' + fieldId ).removeClass( 'frm_hidden' );
3346 } else {
3347 jQuery( '.frm-conf-box-' + fieldId ).addClass( 'frm_hidden' );
3348 setTimeout( function() {
3349 $thisField.removeClass( 'frm_conf_inline frm_conf_below' );
3350 }, 200 );
3351 }
3352 }
3353
3354 function setConfirmationFieldDescriptions( fieldId ) {
3355 const fieldType = document.getElementsByName( 'field_options[type_' + fieldId + ']' )[0].value;
3356
3357 const fieldDescription = document.getElementById( 'field_description_' + fieldId );
3358 const hiddenDescName = 'field_options[description_' + fieldId + ']';
3359 const newValue = frmAdminJs['enter_' + fieldType];
3360 maybeSetNewDescription( fieldDescription, hiddenDescName, newValue );
3361
3362 const confFieldDescription = document.getElementById( 'conf_field_description_' + fieldId );
3363 const hiddenConfName = 'field_options[conf_desc_' + fieldId + ']';
3364 const newConfValue = frmAdminJs['confirm_' + fieldType];
3365 maybeSetNewDescription( confFieldDescription, hiddenConfName, newConfValue );
3366 }
3367
3368 function maybeSetNewDescription( descriptionDiv, hiddenName, newValue ) {
3369 if ( descriptionDiv.innerHTML === frmAdminJs.desc ) {
3370
3371 // Set the visible description value and the hidden description value
3372 descriptionDiv.innerHTML = newValue;
3373 document.getElementsByName( hiddenName )[0].value = newValue;
3374 }
3375 }
3376
3377 function initBulkOptionsOverlay() {
3378 /*jshint validthis:true */
3379 const $info = initModal( '#frm-bulk-modal', '700px' );
3380 if ( $info === false ) {
3381 return;
3382 }
3383
3384 jQuery( '.frm-insert-preset' ).on( 'click', insertBulkPreset );
3385
3386 jQuery( builderForm ).on( 'click', 'a.frm-bulk-edit-link', function( event ) {
3387 event.preventDefault();
3388 let i, key, label,
3389 content = '',
3390 optList,
3391 opts,
3392 fieldId = jQuery( this ).closest( '[data-fid]' ).data( 'fid' ),
3393 separate = usingSeparateValues( fieldId ),
3394 product = isProductField( fieldId );
3395
3396 optList = document.getElementById( 'frm_field_' + fieldId + '_opts' );
3397 if ( ! optList ) {
3398 return;
3399 }
3400
3401 opts = optList.getElementsByTagName( 'li' );
3402
3403 document.getElementById( 'bulk-field-id' ).value = fieldId;
3404
3405 for ( i = 0; i < opts.length; i++ ) {
3406 key = opts[i].getAttribute( 'data-optkey' );
3407 if ( key !== '000' ) {
3408 label = document.getElementsByName( 'field_options[options_' + fieldId + '][' + key + '][label]' )[0];
3409 if ( typeof label !== 'undefined' ) {
3410 content += label.value;
3411 if ( separate ) {
3412 content += '|' + document.getElementsByName( 'field_options[options_' + fieldId + '][' + key + '][value]' )[0].value;
3413 }
3414 if ( product ) {
3415 content += '|' + document.getElementsByName( 'field_options[options_' + fieldId + '][' + key + '][price]' )[0].value;
3416 }
3417 content += '\r\n';
3418 }
3419 }
3420
3421 if ( i >= opts.length - 1 ) {
3422 document.getElementById( 'frm_bulk_options' ).value = content;
3423 }
3424 }
3425
3426 $info.dialog( 'open' );
3427
3428 return false;
3429 });
3430
3431 jQuery( '#frm-update-bulk-opts' ).on( 'click', function() {
3432 const fieldId = document.getElementById( 'bulk-field-id' ).value;
3433 const optionType = document.getElementById( 'bulk-option-type' ).value;
3434
3435 if ( optionType ) {
3436 // Use custom handler for custom option type.
3437 return;
3438 }
3439
3440 this.classList.add( 'frm_loading_button' );
3441 frmAdminBuild.updateOpts( fieldId, document.getElementById( 'frm_bulk_options' ).value, $info );
3442 fieldUpdated();
3443 });
3444 }
3445
3446 function insertBulkPreset( event ) {
3447 /*jshint validthis:true */
3448 const opts = JSON.parse( this.getAttribute( 'data-opts' ) );
3449 event.preventDefault();
3450 document.getElementById( 'frm_bulk_options' ).value = opts.join( '\n' );
3451 return false;
3452 }
3453
3454 //Add new option or "Other" option to radio/checkbox/dropdown
3455 function addFieldOption() {
3456 /*jshint validthis:true */
3457 let fieldId = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' ),
3458 newOption = jQuery( '#frm_field_' + fieldId + '_opts .frm_option_template' ).prop( 'outerHTML' ),
3459 optType = jQuery( this ).data( 'opttype' ),
3460 optKey = 0,
3461 oldKey = '000',
3462 lastKey = getHighestOptKey( fieldId );
3463
3464 if ( lastKey !== oldKey ) {
3465 optKey = lastKey + 1;
3466 }
3467
3468 //Update hidden field
3469 if ( optType === 'other' ) {
3470 document.getElementById( 'other_input_' + fieldId ).value = 1;
3471
3472 //Hide "Add Other" option now if this is radio field
3473 const ftype = jQuery( this ).data( 'ftype' );
3474 if ( ftype === 'radio' || ftype === 'select' ) {
3475 jQuery( this ).fadeOut( 'slow' );
3476 }
3477
3478 const data = {
3479 action: 'frm_add_field_option',
3480 field_id: fieldId,
3481 opt_key: optKey,
3482 opt_type: optType,
3483 nonce: frmGlobal.nonce
3484 };
3485 jQuery.post( ajaxurl, data, function( msg ) {
3486 jQuery( document.getElementById( 'frm_field_' + fieldId + '_opts' ) ).append( msg );
3487 resetDisplayedOpts( fieldId );
3488 });
3489 } else {
3490 newOption = newOption.replace( new RegExp( 'optkey="' + oldKey + '"', 'g' ), 'optkey="' + optKey + '"' );
3491 newOption = newOption.replace( new RegExp( '-' + oldKey + '_', 'g' ), '-' + optKey + '_' );
3492 newOption = newOption.replace( new RegExp( '-' + oldKey + '"', 'g' ), '-' + optKey + '"' );
3493 newOption = newOption.replace( new RegExp( '\\[' + oldKey + '\\]', 'g' ), '[' + optKey + ']' );
3494 newOption = newOption.replace( 'frm_hidden frm_option_template', '' );
3495 newOption = { newOption };
3496 addSaveAndDragIconsToOption( fieldId, newOption );
3497 jQuery( document.getElementById( 'frm_field_' + fieldId + '_opts' ) ).append( newOption.newOption );
3498 resetDisplayedOpts( fieldId );
3499 }
3500 fieldUpdated();
3501 }
3502
3503 function getHighestOptKey( fieldId ) {
3504 let i = 0,
3505 optKey = 0,
3506 opts = jQuery( '#frm_field_' + fieldId + '_opts li' ),
3507 lastKey = 0;
3508
3509 for ( i; i < opts.length; i++ ) {
3510 optKey = opts[i].getAttribute( 'data-optkey' );
3511 if ( opts.length === 1 ) {
3512 return optKey;
3513 }
3514 if ( optKey !== '000' ) {
3515 optKey = optKey.replace( 'other_', '' );
3516 optKey = parseInt( optKey, 10 );
3517 }
3518
3519 if ( ! isNaN( lastKey ) && ( optKey > lastKey || lastKey === '000' ) ) {
3520 lastKey = optKey;
3521 }
3522 }
3523
3524 return lastKey;
3525 }
3526
3527 function toggleMultSel() {
3528 /*jshint validthis:true */
3529 const fieldId = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' );
3530 toggleMultiSelect( fieldId, this.value );
3531 }
3532
3533 function toggleMultiSelect( fieldId, value ) {
3534 const setting = jQuery( '.frm_multiple_cont_' + fieldId );
3535 if ( value === 'select' ) {
3536 setting.fadeIn( 'fast' );
3537 } else {
3538 setting.fadeOut( 'fast' );
3539 }
3540 }
3541
3542 function toggleSepValues() {
3543 /*jshint validthis:true */
3544 const fieldId = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' );
3545 toggle( jQuery( '.field_' + fieldId + '_option_key' ) );
3546 jQuery( '.field_' + fieldId + '_option' ).toggleClass( 'frm_with_key' );
3547 }
3548
3549 function toggleImageOptions() {
3550 /*jshint validthis:true */
3551 let hasImageOptions, imageSize,
3552 $field = jQuery( this ).closest( '.frm-single-settings' ),
3553 fieldId = $field.data( 'fid' ),
3554 displayField = document.getElementById( 'frm_field_id_' + fieldId );
3555
3556 refreshOptionDisplayNow( jQuery( this ) );
3557
3558 toggle( jQuery( '.field_' + fieldId + '_image_id' ) );
3559 toggle( jQuery( '.frm_toggle_image_options_' + fieldId ) );
3560 toggle( jQuery( '.frm_image_size_' + fieldId ) );
3561 toggle( jQuery( '.frm_alignment_' + fieldId ) );
3562 toggle( jQuery( '.frm-add-other#frm_add_field_' + fieldId ) );
3563
3564 hasImageOptions = imagesAsOptions( fieldId );
3565
3566 if ( hasImageOptions ) {
3567 setAlignment( fieldId, 'inline' );
3568 removeImageSizeClasses( displayField );
3569 imageSize = getImageOptionSize( fieldId );
3570 displayField.classList.add( 'frm_image_options' );
3571 displayField.classList.add( 'frm_image_size_' + imageSize );
3572 $field.find( '.frm-bulk-edit-link' ).hide();
3573 } else {
3574 displayField.classList.remove( 'frm_image_options' );
3575 removeImageSizeClasses( displayField );
3576 setAlignment( fieldId, 'block' );
3577 $field.find( '.frm-bulk-edit-link' ).show();
3578 }
3579 }
3580
3581 function removeImageSizeClasses( field ) {
3582 field.classList.remove( 'frm_image_size_', 'frm_image_size_small', 'frm_image_size_medium', 'frm_image_size_large', 'frm_image_size_xlarge' );
3583 }
3584
3585 function setAlignment( fieldId, alignment ) {
3586 jQuery( '#field_options_align_' + fieldId ).val( alignment ).trigger( 'change' );
3587 }
3588
3589 function setImageSize() {
3590 const $field = jQuery( this ).closest( '.frm-single-settings' ),
3591 fieldId = $field.data( 'fid' ),
3592 displayField = document.getElementById( 'frm_field_id_' + fieldId );
3593
3594 refreshOptionDisplay();
3595
3596 if ( imagesAsOptions( fieldId ) ) {
3597 removeImageSizeClasses( displayField );
3598 displayField.classList.add( 'frm_image_options' );
3599 displayField.classList.add( 'frm_image_size_' + getImageOptionSize( fieldId ) );
3600 }
3601 }
3602
3603 function refreshOptionDisplayNow( object ) {
3604 const $field = object.closest( '.frm-single-settings' ),
3605 fieldID = $field.data( 'fid' );
3606 jQuery( '.field_' + fieldID + '_option' ).trigger( 'change' );
3607 }
3608
3609 function refreshOptionDisplay() {
3610 /*jshint validthis:true */
3611 refreshOptionDisplayNow( jQuery( this ) );
3612 }
3613
3614 function addImageToOption( event ) {
3615 const imagePreview = event.target.closest( '.frm_image_preview_wrapper' );
3616
3617 event.preventDefault();
3618
3619 wp.media.model.settings.post.id = 0;
3620
3621 const fileFrame = wp.media.frames.file_frame = wp.media({
3622 multiple: false,
3623 library: {
3624 type: [ 'image' ]
3625 }
3626 });
3627
3628 fileFrame.on( 'select', function() {
3629 const attachment = fileFrame.state().get( 'selection' ).first().toJSON();
3630 const img = imagePreview.querySelector( 'img' );
3631
3632 img.setAttribute( 'src', attachment.url );
3633 img.classList.remove( 'frm_hidden' );
3634 img.removeAttribute( 'srcset' ); // Prevent the old image from sticking around.
3635
3636 imagePreview.querySelector( '.frm_image_preview_frame' ).style.display = 'block';
3637 imagePreview.querySelector( '.frm_image_preview_title' ).textContent = attachment.filename;
3638 imagePreview.querySelector( '.frm_choose_image_box' ).style.display = 'none';
3639
3640 const $imagePreview = jQuery( imagePreview );
3641 $imagePreview.siblings( 'input[name*="[label]"]' ).data( 'frmimgurl', attachment.url );
3642 $imagePreview.find( 'input.frm_image_id' ).val( attachment.id ).trigger( 'change' );
3643 wp.media.model.settings.post.id = 0;
3644 });
3645
3646 fileFrame.open();
3647 }
3648
3649 function removeImageFromOption( event ) {
3650 const $this = jQuery( this ),
3651 previewWrapper = $this.closest( '.frm_image_preview_wrapper' );
3652
3653 event.preventDefault();
3654 event.stopPropagation();
3655
3656 previewWrapper.find( 'img' ).attr( 'src', '' );
3657 previewWrapper.find( '.frm_image_preview_frame' ).hide();
3658 previewWrapper.find( '.frm_choose_image_box' ).show();
3659 previewWrapper.find( 'input.frm_image_id' ).val( 0 ).trigger( 'change' );
3660 }
3661
3662 function toggleMultiselect() {
3663 /*jshint validthis:true */
3664 const dropdown = jQuery( this ).closest( 'li' ).find( '.frm_form_fields select' );
3665 if ( this.checked ) {
3666 dropdown.attr( 'multiple', 'multiple' );
3667 } else {
3668 dropdown.removeAttr( 'multiple' );
3669 }
3670 }
3671
3672 /**
3673 * Allow typing on form switcher click without an extra click to search.
3674 */
3675 function focusSearchBox() {
3676 const searchBox = document.getElementById( 'dropform-search-input' );
3677 if ( searchBox !== null ) {
3678 setTimeout( function() {
3679 searchBox.focus();
3680 }, 100 );
3681 }
3682 }
3683
3684 /**
3685 * Dismiss a warning message and send an AJAX request to update the dismissal state.
3686 *
3687 * @since 6.3
3688 *
3689 * @param {Event} event The event object associated with the click on the dismiss icon.
3690 */
3691 function dismissWarningMessage( event ) {
3692 const target = event.target;
3693
3694 const warningEl = target.closest( '.frm_warning_style' );
3695 jQuery( warningEl ).fadeOut( 400, () => warningEl.remove() );
3696
3697 const action = target.dataset.action;
3698 const formData = new FormData();
3699 doJsonPost( action, formData );
3700 }
3701
3702 /**
3703 * If a field is clicked in the builder, prevent inputs from changing.
3704 */
3705 function stopFieldFocus( e ) {
3706 e.preventDefault();
3707 }
3708
3709 function deleteFieldOption() {
3710 /*jshint validthis:true */
3711 let otherInput,
3712 parentLi = this.parentNode,
3713 parentUl = parentLi.parentNode,
3714 fieldId = this.getAttribute( 'data-fid' );
3715
3716 jQuery( parentLi ).fadeOut( 'slow', function() {
3717 wp.hooks.doAction( 'frm_before_delete_field_option', this );
3718 jQuery( parentLi ).remove();
3719
3720 const hasOther = jQuery( parentUl ).find( '.frm_other_option' );
3721 if ( hasOther.length < 1 ) {
3722 otherInput = document.getElementById( 'other_input_' + fieldId );
3723 if ( otherInput !== null ) {
3724 otherInput.value = 0;
3725 }
3726 jQuery( '#other_button_' + fieldId ).fadeIn( 'slow' );
3727 }
3728 });
3729 fieldUpdated();
3730 }
3731
3732 /**
3733 * If a radio button is set as default, allow a click to
3734 * deselect it.
3735 */
3736 function maybeUncheckRadio() {
3737 let $self, uncheck, unbind, up;
3738
3739 /*jshint validthis:true */
3740 $self = jQuery( this );
3741 if ( $self.is( ':checked' ) ) {
3742 uncheck = function() {
3743 setTimeout( function() {
3744 $self.prop( 'checked', false );
3745 }, 0 );
3746 };
3747 unbind = function() {
3748 $self.off( 'mouseup', up );
3749 };
3750 up = function() {
3751 uncheck();
3752 unbind();
3753 };
3754 $self.on( 'mouseup', up );
3755 $self.one( 'mouseout', unbind );
3756 }
3757 }
3758
3759 /**
3760 * If the field option has the default text, clear it out on click.
3761 */
3762 function maybeClearOptText() {
3763 /*jshint validthis:true */
3764 if ( this.value === frmAdminJs.new_option ) {
3765 this.setAttribute( 'data-value-on-focus', this.value );
3766 this.value = '';
3767 }
3768 }
3769
3770 function confirmFieldsDeleteMessage( numberOfFields ) {
3771 /* translators: %1$s: Number of fields that are selected to be deleted. */
3772 return sprintf( __( 'Are you sure you want to delete these %1$s selected field(s)?', 'formidable' ), numberOfFields );
3773 }
3774
3775 function clickDeleteField() {
3776 /*jshint validthis:true */
3777 let confirmMsg = frmAdminJs.conf_delete,
3778 maybeDivider = this.parentNode.parentNode.parentNode.parentNode.parentNode,
3779 li = maybeDivider.parentNode,
3780 field = jQuery( this ).closest( 'li.form-field' ),
3781 fieldId = field.data( 'fid' );
3782
3783 if ( field.data( 'ftype' ) === 'divider' ) {
3784 const fieldBoxes = document.querySelectorAll( '.frm-field-group-hover-target .start_divider .frm_field_box' );
3785 let fieldIdsToDelete = 0;
3786 fieldBoxes.forEach( fieldBox => {
3787 const fieldsInsideFieldBox = fieldBox.querySelectorAll( 'li.form-field' );
3788 if ( fieldsInsideFieldBox ) {
3789 fieldIdsToDelete += fieldsInsideFieldBox.length;
3790 }
3791 });
3792 if ( fieldIdsToDelete ) {
3793 confirmMsg = confirmFieldsDeleteMessage( ++fieldIdsToDelete );
3794 }
3795 }
3796
3797 if ( li.classList.contains( 'frm-section-collapsed' ) || li.classList.contains( 'frm-page-collapsed' ) ) {
3798 return false;
3799 }
3800
3801 // If deleting a section, use a special message.
3802 if ( maybeDivider.className === 'divider_section_only' ) {
3803 confirmMsg = frmAdminJs.conf_delete_sec;
3804 }
3805
3806 this.setAttribute( 'data-frmverify', confirmMsg );
3807 this.setAttribute( 'data-frmverify-btn', 'frm-button-red' );
3808 this.setAttribute( 'data-deletefield', fieldId );
3809
3810 closeOpenFieldDropdowns();
3811
3812 confirmLinkClick( this );
3813 return false;
3814 }
3815
3816 function clickSelectField() {
3817 this.closest( 'li.form-field' ).click();
3818 }
3819
3820 function clickDeleteFieldGroup() {
3821 let hoverTarget, decoy;
3822
3823 hoverTarget = document.querySelector( '.frm-field-group-hover-target' );
3824 if ( null === hoverTarget ) {
3825 return;
3826 }
3827
3828 hoverTarget.classList.add( 'frm-selected-field-group' );
3829
3830 decoy = document.createElement( 'div' );
3831 decoy.classList.add( 'frm-delete-field-groups', 'frm_hidden' );
3832 document.body.appendChild( decoy );
3833 decoy.click();
3834 }
3835
3836 function duplicateFieldGroup() {
3837 const hoverTarget = document.querySelector( '.frm-field-group-hover-target' );
3838 if ( null === hoverTarget ) {
3839 return;
3840 }
3841
3842 const newRowId = 'frm_field_group_' + getAutoId();
3843 const placeholderUlChild = document.createTextNode( '' );
3844 wrapFieldLiInPlace( placeholderUlChild );
3845
3846 const newRow = jQuery( placeholderUlChild ).closest( 'li' ).get( 0 );
3847 newRow.classList.add( 'frm_hidden' );
3848
3849 const newRowUl = newRow.querySelector( 'ul' );
3850 newRowUl.id = newRowId;
3851
3852 jQuery( hoverTarget.closest( 'li.frm_field_box' ) ).after( newRow );
3853
3854 const $fields = getFieldsInRow( jQuery( hoverTarget ) );
3855 const syncDetails = [];
3856 const injectedCloneOptions = [];
3857
3858 const expectedLength = $fields.length;
3859 const originalFieldIdByDuplicatedFieldId = {};
3860
3861 let duplicatedCount = 0;
3862
3863 jQuery( newRow ).on(
3864 'frm_added_duplicated_field_to_row',
3865 function( _, args ) {
3866 originalFieldIdByDuplicatedFieldId[ jQuery( args.duplicatedFieldHtml ).attr( 'data-fid' ) ] = args.originalFieldId;
3867
3868 if ( expectedLength > ++duplicatedCount ) {
3869 return;
3870 }
3871
3872 const $newRowUl = jQuery( newRowUl );
3873 const $duplicatedFields = getFieldsInRow( $newRowUl );
3874
3875 injectedCloneOptions.forEach(
3876 function( cloneOption ) {
3877 cloneOption.remove();
3878 }
3879 );
3880
3881 for ( let index = 0; index < expectedLength; ++index ) {
3882 $newRowUl.append( $newRowUl.children( 'li.form-field[frm-field-order="' + index + '"]' ) );
3883 }
3884
3885 syncLayoutClasses( $duplicatedFields.first(), syncDetails );
3886 newRow.classList.remove( 'frm_hidden' );
3887 updateFieldOrder();
3888
3889 getFieldsInRow( $newRowUl ).each(
3890 function() {
3891 maybeDuplicateUnsavedSettings( originalFieldIdByDuplicatedFieldId[ this.getAttribute( 'data-fid' ) ], jQuery( this ).prop( 'outerHTML' ) );
3892 }
3893 );
3894 }
3895 );
3896
3897 $fields.each(
3898 function( index ) {
3899 let cloneOption;
3900 cloneOption = document.createElement( 'li' );
3901 cloneOption.classList.add( 'frm_clone_field' );
3902 cloneOption.setAttribute( 'frm-target-row-id', newRowId );
3903 cloneOption.setAttribute( 'frm-field-order', index );
3904 this.appendChild( cloneOption );
3905 cloneOption.click();
3906 injectedCloneOptions.push( cloneOption );
3907 syncDetails.push( getSizeOfLayoutClass( getLayoutClassName( this.classList ) ) );
3908 }
3909 );
3910 }
3911
3912 function clickFieldGroupLayout() {
3913 let hoverTarget, sizeOfFieldGroup, popupWrapper;
3914
3915 hoverTarget = document.querySelector( '.frm-field-group-hover-target' );
3916
3917 if ( null === hoverTarget ) {
3918 return;
3919 }
3920
3921 deselectFields();
3922
3923 sizeOfFieldGroup = getSizeOfFieldGroupFromChildElement( hoverTarget.querySelector( 'li.form-field' ) );
3924
3925 hoverTarget.classList.add( 'frm-has-open-field-group-popup' );
3926 jQuery( document ).on( 'click', '#frm_builder_page', destroyFieldGroupPopupOnOutsideClick );
3927
3928 popupWrapper = div();
3929 popupWrapper.style.position = 'relative';
3930 popupWrapper.appendChild( getFieldGroupPopup( sizeOfFieldGroup, this ) );
3931 this.parentNode.appendChild( popupWrapper );
3932
3933 const firstLayoutOption = popupWrapper.querySelector( '.frm-row-layout-option' );
3934 if ( firstLayoutOption ) {
3935 firstLayoutOption.focus();
3936 }
3937 }
3938
3939 function destroyFieldGroupPopupOnOutsideClick( event ) {
3940 if ( event.target.classList.contains( 'frm-custom-field-group-layout' ) || event.target.classList.contains( 'frm-cancel-custom-field-group-layout' ) ) {
3941 return;
3942 }
3943 if ( ! jQuery( event.target ).closest( '#frm_field_group_controls' ).length && ! jQuery( event.target ).closest( '#frm_field_group_popup' ).length ) {
3944 destroyFieldGroupPopup();
3945 }
3946 }
3947
3948 function getSizeOfFieldGroupFromChildElement( element ) {
3949 const $ul = jQuery( element ).closest( 'ul' );
3950 if ( $ul.length ) {
3951 return getFieldsInRow( $ul ).length;
3952 }
3953 return getSelectedFieldCount();
3954 }
3955
3956 function getFieldGroupPopup( sizeOfFieldGroup, childElement ) {
3957 let popup, wrapper, rowLayoutOptions, ul;
3958
3959 popup = document.getElementById( 'frm_field_group_popup' );
3960 if ( null === popup ) {
3961 popup = div();
3962 } else {
3963 popup.innerHTML = '';
3964 }
3965
3966 popup.id = 'frm_field_group_popup';
3967
3968 wrapper = div();
3969 wrapper.style.padding = '0 24px 12px';
3970 wrapper.appendChild( getRowLayoutTitle() );
3971
3972 rowLayoutOptions = getRowLayoutOptions( sizeOfFieldGroup );
3973
3974 ul = childElement.closest( 'ul.frm_sorting' );
3975 if ( null !== ul ) {
3976 maybeMarkRowLayoutAsActive( ul, rowLayoutOptions );
3977 }
3978
3979 wrapper.appendChild( rowLayoutOptions );
3980
3981 popup.appendChild( wrapper );
3982 popup.appendChild( separator() );
3983
3984 popup.appendChild( getCustomLayoutOption() );
3985 popup.appendChild( getBreakIntoDifferentRowsOption() );
3986
3987 return popup;
3988 }
3989
3990 function maybeMarkRowLayoutAsActive( activeRow, options ) {
3991 let length, index, currentRow;
3992
3993 length = options.children.length;
3994 for ( index = 0; index < length; ++index ) {
3995 currentRow = options.children[ index ];
3996 if ( rowLayoutsMatch( currentRow, activeRow ) ) {
3997 currentRow.classList.add( 'frm-active-row-layout' );
3998 return;
3999 }
4000 }
4001 }
4002
4003 function separator() {
4004 return document.createElement( 'hr' );
4005 }
4006
4007 function getCustomLayoutOption() {
4008 const option = div();
4009 option.textContent = __( 'Custom layout', 'formidable' );
4010 jQuery( option ).prepend( getIconClone( 'frm_gear_svg' ) );
4011 option.classList.add( 'frm-custom-field-group-layout' );
4012 makeTabbable( option );
4013 return option;
4014 }
4015
4016 function makeTabbable( element, ariaLabel ) {
4017 element.setAttribute( 'tabindex', 0 );
4018 element.setAttribute( 'role', 'button' );
4019 if ( 'undefined' !== typeof ariaLabel ) {
4020 element.setAttribute( 'aria-label', ariaLabel );
4021 }
4022 }
4023
4024 function getIconClone( iconId ) {
4025 const clone = document.getElementById( iconId ).cloneNode( true );
4026 clone.id = '';
4027 return clone;
4028 }
4029
4030 function getBreakIntoDifferentRowsOption() {
4031 const option = div();
4032 option.textContent = __( 'Break into rows', 'formidable' );
4033 jQuery( option ).prepend( getIconClone( 'frm_break_field_group_svg' ) );
4034 option.classList.add( 'frm-break-field-group' );
4035 makeTabbable( option );
4036 return option;
4037 }
4038
4039 function getRowLayoutTitle() {
4040 const rowLayoutTitle = div();
4041 rowLayoutTitle.classList.add( 'frm-row-layout-title' );
4042 rowLayoutTitle.textContent = __( 'Row Layout', 'formidable' );
4043 return rowLayoutTitle;
4044 }
4045
4046 function getRowLayoutOptions( size ) {
4047 let wrapper, padding;
4048
4049 wrapper = getEmptyGridContainer();
4050 if ( 5 !== size ) {
4051 wrapper.appendChild( getRowLayoutOption( size, 'even' ) );
4052 }
4053 if ( size % 2 === 1 ) {
4054 // only include the middle option for odd numbers because even doesn't make a lot of sense.
4055 wrapper.appendChild( getRowLayoutOption( size, 'middle' ) );
4056 }
4057 if ( size < 6 ) {
4058 wrapper.appendChild( getRowLayoutOption( size, 'left' ) );
4059 wrapper.appendChild( getRowLayoutOption( size, 'right' ) );
4060 } else {
4061 padding = div();
4062 padding.classList.add( 'frm_fourth' );
4063 wrapper.prepend( padding );
4064 }
4065
4066 return wrapper;
4067 }
4068
4069 function getRowLayoutOption( size, type ) {
4070 let option, useClass;
4071
4072 option = div();
4073 option.classList.add( 'frm-row-layout-option' );
4074 makeTabbable( option, type );
4075
4076 switch ( size ) {
4077 case 6:
4078 useClass = 'frm_half';
4079 break;
4080 case 5:
4081 useClass = 'frm_third';
4082 break;
4083 default:
4084 useClass = size % 2 === 1 ? 'frm_fourth' : 'frm_third';
4085 break;
4086 }
4087
4088 option.classList.add( useClass );
4089 option.setAttribute( 'layout-type', type );
4090
4091 option.appendChild( getRowForSizeAndType( size, type ) );
4092 return option;
4093 }
4094
4095 function rowLayoutsMatch( row1, row2 ) {
4096 return getRowLayoutAsKey( row1 ) === getRowLayoutAsKey( row2 );
4097 }
4098
4099 function getRowLayoutAsKey( row ) {
4100 let $fields, sizes;
4101 if ( row.classList.contains( 'frm-row-layout-option' ) ) {
4102 $fields = jQuery( row ).find( '.frm_grid_container' ).children();
4103 } else {
4104 $fields = getFieldsInRow( jQuery( row ) );
4105 }
4106 sizes = [];
4107 $fields.each(
4108 function() {
4109 sizes.push( getSizeOfLayoutClass( getLayoutClassName( this.classList ) ) );
4110 }
4111 );
4112 return sizes.join( '-' );
4113 }
4114
4115 function getRowForSizeAndType( size, type ) {
4116 let row, index, block;
4117
4118 row = getEmptyGridContainer();
4119 for ( index = 0; index < size; ++index ) {
4120 block = div();
4121 block.classList.add( getClassForBlock( size, type, index ) );
4122 block.style.height = '16px';
4123 block.style.background = '#9EA9B8';
4124 block.style.borderRadius = '1px';
4125 row.appendChild( block );
4126 }
4127
4128 return row;
4129 }
4130
4131 /**
4132 * @param {int} size 2-6.
4133 * @param {string} type even, middle, left, or right.
4134 * @param {int} index 0-5.
4135 * @returns string
4136 */
4137 function getClassForBlock( size, type, index ) {
4138 if ( 'even' === type ) {
4139 return getEvenClassForSize( size, index );
4140 } else if ( 'middle' === type ) {
4141 if ( 3 === size ) {
4142 return 1 === index ? 'frm6' : 'frm3';
4143 }
4144 if ( 5 === size ) {
4145 return 2 === index ? 'frm4' : 'frm2';
4146 }
4147 } else if ( 'left' === type ) {
4148 return 0 === index ? getLargeClassForSize( size ) : getSmallClassForSize( size );
4149 } else if ( 'right' === type ) {
4150 return index === size - 1 ? getLargeClassForSize( size ) : getSmallClassForSize( size );
4151 }
4152 return 'frm12';
4153 }
4154
4155 function getEvenClassForSize( size, index ) {
4156 if ( -1 !== [ 2, 3, 4, 6 ].indexOf( size ) ) {
4157 return getLayoutClassForSize( 12 / size );
4158 }
4159 if ( 5 === size && 'undefined' !== typeof index ) {
4160 return 0 === index ? 'frm4' : 'frm2';
4161 }
4162 return 'frm12';
4163 }
4164
4165 function getSmallClassForSize( size ) {
4166 switch ( size ) {
4167 case 2: case 3:
4168 return 'frm3';
4169 case 4:
4170 return 'frm2';
4171 case 5:
4172 return 'frm2';
4173 case 6:
4174 return 'frm1';
4175 }
4176 return 'frm12';
4177 }
4178
4179 function getLargeClassForSize( size ) {
4180 switch ( size ) {
4181 case 2:
4182 return 'frm9';
4183 case 3: case 4:
4184 return 'frm6';
4185 case 5:
4186 return 'frm4';
4187 case 6:
4188 return 'frm7';
4189 }
4190 return 'frm12';
4191 }
4192
4193 function getEmptyGridContainer() {
4194 const wrapper = div();
4195 wrapper.classList.add( 'frm_grid_container' );
4196 return wrapper;
4197 }
4198
4199 /**
4200 * Handle when a field group layout option (that sets grid classes/column sizing) is selected in the "Row Layout" popup.
4201 *
4202 * @returns {void}
4203 */
4204 function handleFieldGroupLayoutOptionClick() {
4205 const row = document.querySelector( '.frm-field-group-hover-target' );
4206 if ( ! row ) {
4207 // The field group layout options also get clicked when merging multiple rows.
4208 // The following code isn't required for multiple rows though so just exit early.
4209 return;
4210 }
4211
4212 const type = this.getAttribute( 'layout-type' );
4213 syncLayoutClasses( getFieldsInRow( jQuery( row ) ).first(), type );
4214 destroyFieldGroupPopup();
4215 }
4216
4217 function handleFieldGroupLayoutOptionInsideMergeClick() {
4218 let $ul, type;
4219 $ul = mergeSelectedFieldGroups();
4220 type = this.getAttribute( 'layout-type' );
4221 syncLayoutClasses( getFieldsInRow( $ul ).first(), type );
4222 unselectFieldGroups();
4223 }
4224
4225 function mergeSelectedFieldGroups() {
4226 const $selectedFieldGroups = jQuery( '.frm-selected-field-group' ),
4227 $firstGroupUl = $selectedFieldGroups.first();
4228 $selectedFieldGroups.not( $firstGroupUl ).each(
4229 function() {
4230 getFieldsInRow( jQuery( this ) ).each(
4231 function() {
4232 const previousParent = this.parentNode;
4233 getFieldsInRow( $firstGroupUl ).last().after( this );
4234 if ( ! jQuery( previousParent ).children( 'li.form-field' ).length ) {
4235 // clean up the previous field group if we've removed all of its fields.
4236 previousParent.closest( 'li.frm_field_box' ).remove();
4237 }
4238 }
4239 );
4240 }
4241 );
4242 updateFieldOrder();
4243 syncLayoutClasses( getFieldsInRow( $firstGroupUl ).first() );
4244 return $firstGroupUl;
4245 }
4246
4247 function customFieldGroupLayoutClick() {
4248 let $fields;
4249 if ( null !== this.closest( '.frm-merge-fields-into-row' ) ) {
4250 return;
4251 }
4252 $fields = getFieldsInRow( jQuery( '.frm-field-group-hover-target' ) );
4253 setupCustomLayoutOptions( $fields );
4254 }
4255
4256 function setupCustomLayoutOptions( $fields ) {
4257 let size, popup, wrapper, layoutClass, inputRow, paddingElement, inputValueOverride, index, inputField, heading, label, buttonsWrapper, cancelButton, saveButton;
4258
4259 size = $fields.length;
4260
4261 popup = document.getElementById( 'frm_field_group_popup' );
4262 popup.innerHTML = '';
4263
4264 wrapper = div();
4265 wrapper.style.padding = '0 24px';
4266
4267 layoutClass = getEvenClassForSize( 5 === size ? 6 : size );
4268
4269 inputRow = div();
4270 inputRow.style.padding = '20px 0';
4271 inputRow.classList.add( 'frm_grid_container' );
4272
4273 if ( 5 === size ) {
4274 // add a span to pad the inputs by 1 column, to account for the missing 2 columns.
4275 paddingElement = document.createElement( 'span' );
4276 paddingElement.classList.add( 'frm1' );
4277 inputRow.appendChild( paddingElement );
4278 }
4279
4280 inputValueOverride = getSelectedFieldCount() > 0 ? getSizeOfLayoutClass( getEvenClassForSize( size ) ) : false;
4281 if ( false !== inputValueOverride && inputValueOverride >= 12 ) {
4282 inputValueOverride = Math.floor( 12 / size );
4283 }
4284
4285 for ( index = 0; index < size; ++index ) {
4286 inputField = document.createElement( 'input' );
4287 inputField.type = 'text';
4288 inputField.classList.add( layoutClass );
4289 inputField.classList.add( 'frm-custom-grid-size-input' );
4290 inputField.value = false !== inputValueOverride ? inputValueOverride : getSizeOfLayoutClass( getLayoutClassName( $fields.get( index ).classList ) );
4291 inputRow.appendChild( inputField );
4292 }
4293
4294 heading = div();
4295 heading.classList.add( 'frm-builder-popup-heading' );
4296 heading.textContent = __( 'Enter number of columns for each field', 'formidable' );
4297
4298 label = div();
4299 label.classList.add( 'frm-builder-popup-subheading' );
4300 label.textContent = __( 'Layouts are based on a 12-column grid system', 'formidable' );
4301
4302 wrapper.appendChild( heading );
4303 wrapper.appendChild( label );
4304
4305 wrapper.appendChild( inputRow );
4306
4307 buttonsWrapper = div();
4308 buttonsWrapper.style.textAlign = 'right';
4309
4310 cancelButton = getSecondaryButton();
4311 cancelButton.textContent = __( 'Cancel', 'formidable' );
4312 cancelButton.classList.add( 'frm-cancel-custom-field-group-layout' );
4313 cancelButton.style.marginRight = '10px';
4314
4315 saveButton = getPrimaryButton();
4316 saveButton.textContent = __( 'Save', 'formidable' );
4317 saveButton.classList.add( 'frm-save-custom-field-group-layout' );
4318
4319 buttonsWrapper.appendChild( cancelButton );
4320 buttonsWrapper.appendChild( saveButton );
4321
4322 wrapper.appendChild( buttonsWrapper );
4323
4324 popup.appendChild( wrapper );
4325
4326 setTimeout(
4327 function() {
4328 const firstInput = popup.querySelector( 'input.frm-custom-grid-size-input' ).focus();
4329 if ( firstInput ) {
4330 firstInput.focus();
4331 }
4332 },
4333 0
4334 );
4335 }
4336
4337 function customFieldGroupLayoutInsideMergeClick() {
4338 $fields = jQuery( '.frm-selected-field-group li.form-field' );
4339 setupCustomLayoutOptions( $fields );
4340 }
4341
4342 function getPrimaryButton() {
4343 const button = getButton();
4344 button.classList.add( 'button-primary', 'frm-button-primary' );
4345 return button;
4346 }
4347
4348 function getSecondaryButton() {
4349 const button = getButton();
4350 button.classList.add( 'button-secondary', 'frm-button-secondary' );
4351 return button;
4352 }
4353
4354 function getButton() {
4355 const button = document.createElement( 'a' );
4356 button.setAttribute( 'href', '#' );
4357 button.classList.add( 'button' );
4358 button.style.textDecoration = 'none';
4359 return button;
4360 }
4361
4362 function getSizeOfLayoutClass( className ) {
4363 switch ( className ) {
4364 case 'frm_half':
4365 return 6;
4366 case 'frm_third':
4367 return 4;
4368 case 'frm_two_thirds':
4369 return 8;
4370 case 'frm_fourth':
4371 return 3;
4372 case 'frm_three_fourths':
4373 return 9;
4374 case 'frm_sixth':
4375 return 2;
4376 }
4377
4378 if ( 0 === className.indexOf( 'frm' ) ) {
4379 return parseInt( className.substr( 3 ) );
4380 }
4381
4382 // Anything missing a layout class should be a full width row.
4383 return 12;
4384 }
4385
4386 function getLayoutClassName( classList ) {
4387 let classes, index, currentClass;
4388 classes = getLayoutClasses();
4389 for ( index = 0; index < classes.length; ++index ) {
4390 currentClass = classes[ index ];
4391 if ( classList.contains( currentClass ) ) {
4392 return currentClass;
4393 }
4394 }
4395 return '';
4396 }
4397
4398 function getLayoutClassForSize( size ) {
4399 return 'frm' + size;
4400 }
4401
4402 function breakFieldGroupClick() {
4403 const row = document.querySelector( '.frm-field-group-hover-target' );
4404 breakRow( row );
4405 destroyFieldGroupPopup();
4406 }
4407
4408 function breakRow( row ) {
4409 const $row = jQuery( row );
4410 getFieldsInRow( $row ).each(
4411 function( index ) {
4412 const field = this;
4413 if ( 0 !== index ) {
4414 $row.parent().after( wrapFieldLi( field ) );
4415 }
4416 stripLayoutFromFields( jQuery( field ) );
4417 }
4418 );
4419 }
4420
4421 function stripLayoutFromFields( field ) {
4422 syncLayoutClasses( field, 'clear' );
4423 }
4424
4425 function focusFieldGroupInputOnClick() {
4426 this.select();
4427 }
4428
4429 function cancelCustomFieldGroupClick() {
4430 revertToFieldGroupPopupFirstPage( this );
4431 }
4432
4433 function revertToFieldGroupPopupFirstPage( triggerElement ) {
4434 jQuery( document.getElementById( 'frm_field_group_popup' ) ).replaceWith(
4435 getFieldGroupPopup( getSizeOfFieldGroupFromChildElement( triggerElement ), triggerElement )
4436 );
4437 }
4438
4439 function destroyFieldGroupPopup() {
4440 let popup, wrapper;
4441 popup = document.getElementById( 'frm_field_group_popup' );
4442 if ( popup === null ) {
4443 return;
4444 }
4445 wrapper = document.querySelector( '.frm-has-open-field-group-popup' );
4446 if ( null !== wrapper ) {
4447 wrapper.classList.remove( 'frm-has-open-field-group-popup' );
4448 popup.parentNode.remove();
4449 }
4450 jQuery( document ).off( 'click', '#frm_builder_page', destroyFieldGroupPopupOnOutsideClick );
4451 }
4452
4453 function saveCustomFieldGroupClick() {
4454 let syncDetails, $controls, $ul;
4455
4456 syncDetails = [];
4457
4458 jQuery( document.getElementById( 'frm_field_group_popup' ).querySelectorAll( '.frm_grid_container input' ) )
4459 .each(
4460 function() {
4461 syncDetails.push( parseInt( this.value ) );
4462 }
4463 );
4464
4465 $controls = jQuery( document.getElementById( 'frm_field_group_controls' ) );
4466
4467 if ( $controls.length && 'none' !== $controls.get( 0 ).style.display ) {
4468 syncLayoutClasses( getFieldsInRow( jQuery( document.querySelector( '.frm-field-group-hover-target' ) ) ).first(), syncDetails );
4469 } else {
4470 $ul = mergeSelectedFieldGroups();
4471 syncLayoutClasses( getFieldsInRow( $ul ).first(), syncDetails );
4472 unselectFieldGroups();
4473 }
4474
4475 destroyFieldGroupPopup();
4476 }
4477
4478 function fieldGroupClick( e ) {
4479 maybeShowFieldGroupMessage();
4480
4481 if ( 'ul' !== e.originalEvent.target.nodeName.toLowerCase() ) {
4482 // only continue if the group itself was clicked / ignore when a field is clicked.
4483 return;
4484 }
4485
4486 const hoverTarget = document.querySelector( '.frm-field-group-hover-target' );
4487 if ( ! hoverTarget ) {
4488 return;
4489 }
4490
4491 const ctrlOrCmdKeyIsDown = e.ctrlKey || e.metaKey;
4492 const shiftKeyIsDown = e.shiftKey;
4493 const groupIsActive = hoverTarget.classList.contains( 'frm-selected-field-group' );
4494 const $selectedFieldGroups = getSelectedFieldGroups();
4495
4496 let numberOfSelectedGroups = $selectedFieldGroups.length;
4497
4498 if ( ctrlOrCmdKeyIsDown || shiftKeyIsDown ) {
4499 // multi-selecting
4500
4501 const selectedField = getSelectedField();
4502 if ( null !== selectedField && ! jQuery( selectedField ).siblings( 'li.form-field' ).length ) {
4503 // count a selected field on its own as a selected field group when multiselecting.
4504 selectedField.parentNode.classList.add( 'frm-selected-field-group' );
4505 ++numberOfSelectedGroups;
4506 }
4507
4508 if ( ctrlOrCmdKeyIsDown ) {
4509 if ( groupIsActive ) {
4510 // unselect if holding ctrl or cmd and the group was already active.
4511 --numberOfSelectedGroups;
4512 hoverTarget.classList.remove( 'frm-selected-field-group' );
4513 syncAfterMultiSelect( numberOfSelectedGroups );
4514 return; // exit early to avoid adding back frm-selected-field-group
4515 }
4516
4517 ++numberOfSelectedGroups;
4518 } else if ( shiftKeyIsDown && ! groupIsActive ) {
4519 ++numberOfSelectedGroups; // include the one we're selecting right now.
4520 const $firstGroup = $selectedFieldGroups.first();
4521
4522 let $range;
4523 if ( $firstGroup.parent().index() < jQuery( hoverTarget.parentNode ).index() ) {
4524 $range = $firstGroup.parent().nextUntil( hoverTarget.parentNode );
4525 } else {
4526 $range = $firstGroup.parent().prevUntil( hoverTarget.parentNode );
4527 }
4528
4529 $range.each(
4530 function() {
4531 const $fieldGroup = jQuery( this ).closest( 'li' ).find( 'ul.frm_sorting' );
4532 if ( ! $fieldGroup.hasClass( 'frm-selected-field-group' ) ) {
4533 ++numberOfSelectedGroups;
4534 $fieldGroup.addClass( 'frm-selected-field-group' );
4535 }
4536 }
4537 );
4538 }
4539 } else {
4540 // not multi-selecting
4541 unselectFieldGroups();
4542 numberOfSelectedGroups = 1;
4543 }
4544
4545 hoverTarget.classList.add( 'frm-selected-field-group' );
4546 syncAfterMultiSelect( numberOfSelectedGroups );
4547
4548 maybeHideFieldGroupMessage();
4549
4550 jQuery( document ).off( 'click', unselectFieldGroups );
4551 jQuery( document ).on( 'click', unselectFieldGroups );
4552 }
4553
4554 /**
4555 * Hide the field group message by manipulating classes.
4556 *
4557 * @param {Element} fieldGroupMessage The field group message element.
4558 * @return {void}
4559 */
4560 function hideFieldGroupMessage( fieldGroupMessage ) {
4561 if ( ! fieldGroupMessage ) {
4562 return;
4563 }
4564
4565 fieldGroupMessage.classList.add( 'frm_hidden' );
4566 fieldGroupMessage.classList.remove( 'frm-fadein-up-back' );
4567 }
4568
4569 /**
4570 * Show the field group message by manipulating classes.
4571 *
4572 * @param {Element} fieldGroupMessage The field group message element.
4573 * @return {void}
4574 */
4575 function showFieldGroupMessage( fieldGroupMessage ) {
4576 if ( ! fieldGroupMessage ) {
4577 return;
4578 }
4579
4580 fieldGroupMessage.classList.remove( 'frm_hidden' );
4581 fieldGroupMessage.classList.add( 'frm-fadein-up-back' );
4582 }
4583
4584 /**
4585 * Maybe show a message if there are at least two rows.
4586 *
4587 * @return {void}
4588 */
4589 function maybeShowFieldGroupMessage() {
4590 let fieldGroupMessage = document.getElementById( 'frm-field-group-message' );
4591 const rows = document.querySelectorAll( '.edit_form_item:not(.edit_field_type_end_divider)' );
4592
4593 if ( rows.length < 2 ) {
4594 hideFieldGroupMessage( fieldGroupMessage );
4595 return;
4596 }
4597
4598 if ( fieldGroupMessage ) {
4599 showFieldGroupMessage( fieldGroupMessage );
4600 return;
4601 }
4602
4603 fieldGroupMessage = div({
4604 id: 'frm-field-group-message',
4605 className: 'frm-flex-center frm-fadein-up-back',
4606 children: [
4607 span({
4608 id: 'frm-field-group-message-dismiss',
4609 className: 'frm-flex-center',
4610 child: svg({ href: '#frm_close_icon' })
4611 })
4612 ]
4613 });
4614
4615 // Insert the field group into the DOM
4616 document.getElementById( 'post-body-content' ).appendChild( fieldGroupMessage );
4617
4618 // Get and add the field group message text
4619 const messageText = getFieldGroupMessageText();
4620 fieldGroupMessage.prepend( messageText );
4621
4622 // Set up a click event listener
4623 document.getElementById( 'frm-field-group-message-dismiss' ).addEventListener( 'click', () => {
4624 hideFieldGroupMessage( document.getElementById( 'frm-field-group-message' ) );
4625 });
4626 }
4627
4628 /**
4629 * Get a span element with text about selecting multiple fields.
4630 *
4631 * @return {HTMLElement} A span element with the message and style classes.
4632 */
4633 function getFieldGroupMessageText() {
4634 const text = document.createElement( 'span' );
4635 text.classList.add( 'frm-field-group-message-text', 'frm-flex-center' );
4636 text.innerHTML = sprintf(
4637 /* translators: %1$s: Start span HTML, %2$s: end span HTML */
4638 frm_admin_js.holdShiftMsg, // eslint-disable-line camelcase
4639 '<span class="frm-meta-tag frm-flex-center"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-shift" viewBox="0 0 16 16"><path d="M7.3 2a1 1 0 0 1 1.4 0l6.4 6.8a1 1 0 0 1-.8 1.7h-2.8v3a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-3H1.7a1 1 0 0 1-.8-1.7L7.3 2zm7 7.5L8 2.7 1.7 9.5h2.8a1 1 0 0 1 1 1v3h5v-3a1 1 0 0 1 1-1h2.8z"/></svg>',
4640 '</span>'
4641 );
4642
4643 return text;
4644 }
4645
4646 /**
4647 * Maybe hide the field group message based on the number of selected rows.
4648 *
4649 * @return {void}
4650 */
4651 function maybeHideFieldGroupMessage() {
4652 const selectedRowCount = document.querySelectorAll( '.frm-selected-field-group' ).length;
4653 if ( selectedRowCount < 2 ) {
4654 return;
4655 }
4656
4657 const fieldGroupMessage = document.getElementById( 'frm-field-group-message' );
4658 hideFieldGroupMessage( fieldGroupMessage );
4659 }
4660
4661 function getSelectedField() {
4662 return document.getElementById( 'frm-show-fields' ).querySelector( 'li.form-field.selected' );
4663 }
4664
4665 function getSelectedFieldGroups() {
4666 const $fieldGroups = jQuery( '.frm-selected-field-group' );
4667 if ( $fieldGroups.length ) {
4668 return $fieldGroups;
4669 }
4670
4671 const selectedField = getSelectedField();
4672 if ( selectedField ) {
4673 // If there is only one field in a group and the field is selected, consider the field's group as selected for multi-select.
4674 const selectedFieldGroup = selectedField.closest( 'ul' );
4675 if ( selectedFieldGroup && 1 === getFieldsInRow( jQuery( selectedFieldGroup ) ).length ) {
4676 selectedFieldGroup.classList.add( 'frm-selected-field-group' );
4677 return jQuery( selectedFieldGroup );
4678 }
4679 }
4680
4681 return jQuery();
4682 }
4683
4684 function syncAfterMultiSelect( numberOfSelectedGroups ) {
4685 clearSettingsBox( true ); // unselect any fields if one is selected.
4686 if ( numberOfSelectedGroups >= 2 || ( 1 === numberOfSelectedGroups && selectedGroupHasMultipleFields() ) ) {
4687 addFieldMultiselectPopup();
4688 } else {
4689 maybeRemoveMultiselectPopup();
4690 }
4691 maybeRemoveGroupHoverTarget();
4692 }
4693
4694 function selectedGroupHasMultipleFields() {
4695 return getFieldsInRow( jQuery( document.querySelector( '.frm-selected-field-group' ) ) ).length > 1;
4696 }
4697
4698 function unselectFieldGroups( event ) {
4699 if ( 'undefined' !== typeof event ) {
4700 if ( null !== event.originalEvent.target.closest( '#frm-show-fields' ) ) {
4701 return;
4702 }
4703 if ( event.originalEvent.target.classList.contains( 'frm-merge-fields-into-row' ) ) {
4704 return;
4705 }
4706 if ( null !== event.originalEvent.target.closest( '.frm-merge-fields-into-row' ) ) {
4707 return;
4708 }
4709 if ( event.originalEvent.target.classList.contains( 'frm-custom-field-group-layout' ) ) {
4710 return;
4711 }
4712 if ( event.originalEvent.target.classList.contains( 'frm-cancel-custom-field-group-layout' ) ) {
4713 return;
4714 }
4715 }
4716 jQuery( '.frm-selected-field-group' ).removeClass( 'frm-selected-field-group' );
4717 jQuery( document ).off( 'click', unselectFieldGroups );
4718 maybeRemoveMultiselectPopup();
4719 }
4720
4721 function maybeRemoveMultiselectPopup() {
4722 const popup = document.getElementById( 'frm_field_multiselect_popup' );
4723 if ( null !== popup ) {
4724 popup.remove();
4725 }
4726 }
4727
4728 function addFieldMultiselectPopup() {
4729 getFieldMultiselectPopup();
4730 }
4731
4732 function getFieldMultiselectPopup() {
4733 let popup, mergeOption, caret, verticalSeparator, deleteOption;
4734
4735 popup = document.getElementById( 'frm_field_multiselect_popup' );
4736
4737 if ( null !== popup ) {
4738 popup.classList.toggle( 'frm-unmergable', ! selectedFieldsAreMergeable() );
4739 return popup;
4740 }
4741
4742 popup = div();
4743 popup.id = 'frm_field_multiselect_popup';
4744 if ( ! selectedFieldsAreMergeable() ) {
4745 popup.classList.add( 'frm-unmergable' );
4746 }
4747
4748 mergeOption = div();
4749 mergeOption.classList.add( 'frm-merge-fields-into-row' );
4750 mergeOption.textContent = __( 'Merge into row', 'formidable' );
4751
4752 caret = document.createElement( 'a' );
4753 caret.style.marginLeft = '5px';
4754 caret.classList.add( 'frm_icon_font', 'frm_arrowdown6_icon' );
4755 caret.setAttribute( 'href', '#' );
4756 mergeOption.appendChild( caret );
4757
4758 popup.appendChild( mergeOption );
4759
4760 verticalSeparator = div();
4761 verticalSeparator.classList.add( 'frm-multiselect-popup-separator' );
4762 popup.appendChild( verticalSeparator );
4763
4764 deleteOption = div();
4765 deleteOption.classList.add( 'frm-delete-field-groups' );
4766 deleteOption.appendChild( getIconClone( 'frm_trash_svg' ) );
4767 popup.appendChild( deleteOption );
4768
4769 document.getElementById( 'post-body-content' ).appendChild( popup );
4770
4771 jQuery( popup ).hide().fadeIn();
4772
4773 return popup;
4774 }
4775
4776 function selectedFieldsAreMergeable() {
4777 let selectedFieldGroups, totalFieldCount, length, index, fieldGroup;
4778 selectedFieldGroups = document.querySelectorAll( '.frm-selected-field-group' );
4779 length = selectedFieldGroups.length;
4780 if ( 1 === length ) {
4781 return false;
4782 }
4783 totalFieldCount = 0;
4784 for ( index = 0; index < length; ++index ) {
4785 fieldGroup = selectedFieldGroups[ index ];
4786 if ( null !== fieldGroup.querySelector( '.edit_field_type_break, .edit_field_type_hidden' ) ) {
4787 return false;
4788 }
4789 totalFieldCount += getFieldsInRow( jQuery( fieldGroup ) ).length;
4790 if ( totalFieldCount > 6 ) {
4791 return false;
4792 }
4793 }
4794 return true;
4795 }
4796
4797 function mergeFieldsIntoRowClick( event ) {
4798 let size, popup;
4799
4800 if ( null !== event.originalEvent.target.closest( '#frm_field_group_popup' ) ) {
4801 // prevent clicks within the popup from triggering the button again.
4802 return;
4803 }
4804
4805 if ( event.originalEvent.target.classList.contains( 'frm-custom-field-group-layout' ) ) {
4806 // avoid switching back to the first page when clicking the custom option nested inside of the merge option.
4807 return;
4808 }
4809
4810 size = getSelectedFieldCount();
4811 popup = getFieldGroupPopup( size, document.querySelector( '.frm-selected-field-group' ).firstChild );
4812 this.appendChild( popup );
4813 }
4814
4815 function getSelectedFieldCount() {
4816 let count = 0;
4817 jQuery( document.querySelectorAll( '.frm-selected-field-group' ) ).each(
4818 function() {
4819 count += getFieldsInRow( jQuery( this ) ).length;
4820 }
4821 );
4822 return count;
4823 }
4824
4825 function deleteFieldGroupsClick() {
4826 let fieldIdsToDelete, deleteOnConfirm, multiselectPopup;
4827
4828 fieldIdsToDelete = getSelectedFieldIds();
4829 deleteOnConfirm = getDeleteSelectedFieldGroupsOnConfirmFunction( fieldIdsToDelete );
4830
4831 multiselectPopup = document.getElementById( 'frm_field_multiselect_popup' );
4832 if ( null !== multiselectPopup ) {
4833 multiselectPopup.remove();
4834 }
4835
4836 this.setAttribute( 'data-frmverify', confirmFieldsDeleteMessage( fieldIdsToDelete.length ) );
4837 confirmLinkClick( this );
4838
4839 const confirmedClick = document.getElementById( 'frm-confirmed-click' );
4840
4841 // Remove any previous delete field data so delete confirmation does not attempt
4842 // to delete a field that was already deleted or previously attempted and cancelled.
4843 confirmedClick?.removeAttribute( 'data-deletefield' );
4844
4845 jQuery( confirmedClick ).on( 'click', deleteOnConfirm );
4846 jQuery( '#frm_confirm_modal' ).one( 'dialogclose', function() {
4847 jQuery( confirmedClick ).off( 'click', deleteOnConfirm );
4848 });
4849 }
4850
4851 function getSelectedFieldIds() {
4852 const deleteFieldIds = [];
4853 jQuery( '.frm-selected-field-group > li.form-field' )
4854 .each(
4855 function() {
4856 deleteFieldIds.push( this.dataset.fid );
4857 }
4858 );
4859 return deleteFieldIds;
4860 }
4861
4862 function getDeleteSelectedFieldGroupsOnConfirmFunction( deleteFieldIds ) {
4863 return function( event ) {
4864 event.preventDefault();
4865 deleteAllSelectedFieldGroups( deleteFieldIds );
4866 };
4867 }
4868
4869 function deleteAllSelectedFieldGroups( deleteFieldIds ) {
4870 deleteFieldIds.forEach(
4871 function( fieldId ) {
4872 deleteFields( fieldId );
4873 }
4874 );
4875 }
4876
4877 function deleteFieldConfirmed() {
4878 /*jshint validthis:true */
4879 deleteFields( this.getAttribute( 'data-deletefield' ) );
4880 }
4881
4882 function deleteFields( fieldId ) {
4883 const field = jQuery( '#frm_field_id_' + fieldId );
4884
4885 deleteField( fieldId );
4886
4887 if ( field.hasClass( 'edit_field_type_divider' ) ) {
4888 field.find( 'li.frm_field_box' ).each( function() {
4889 //TODO: maybe delete only end section
4890 //if(n.hasClass('edit_field_type_end_divider')){
4891 deleteField( this.getAttribute( 'data-fid' ) );
4892 //}
4893 });
4894 }
4895 toggleSectionHolder();
4896 }
4897
4898 /**
4899 * Checks if there is only submit field in the form builder.
4900 *
4901 * @return {Boolean}
4902 */
4903 function hasOnlySubmitField() {
4904 // If there are at least 2 rows, return false.
4905 if ( $newFields.get( 0 ).childElementCount > 1 ) {
4906 return false;
4907 }
4908
4909 const childUl = $newFields.get( 0 ).firstElementChild.firstElementChild;
4910
4911 // Use query instead of children because there might be a div inside this ul.
4912 const childLi = childUl.querySelectorAll( 'li.frm_field_box' );
4913
4914 // If there are at least 2 items in the row, return false.
4915 if ( childLi.length > 1 ) {
4916 return false;
4917 }
4918
4919 return childLi[0].classList.contains( 'edit_field_type_submit' );
4920 }
4921
4922 /**
4923 * Moves open modals out of the field options form.
4924 *
4925 * When a modal is open, it is moved in the DOM and appended to the parent element of the modal trigger input. That
4926 * creates a problem since deleting the field also deletes the modal and this function fixes that problem.
4927 *
4928 * @since 6.22
4929 *
4930 * @param {Object} settings
4931 * @returns {void}
4932 */
4933 function moveOpenModalsOutOfFieldOptions( settings ) {
4934 const openModals = settings[0].querySelectorAll( '.frm-inline-modal[data-fills]' );
4935 if ( ! openModals.length ) {
4936 return;
4937 }
4938 openModals.forEach( modal => {
4939 modal.classList.add( 'frm_hidden' );
4940 modal.removeAttribute( 'data-fills' );
4941 modal.closest( 'form' ).appendChild( modal );
4942 });
4943 }
4944
4945 function deleteField( fieldId ) {
4946 jQuery.ajax({
4947 type: 'POST',
4948 url: ajaxurl,
4949 data: {
4950 action: 'frm_delete_field',
4951 field_id: fieldId,
4952 nonce: frmGlobal.nonce
4953 },
4954 success: function() {
4955 const $thisField = jQuery( document.getElementById( 'frm_field_id_' + fieldId ) ),
4956 settings = jQuery( '#frm-single-settings-' + fieldId );
4957
4958 // Remove settings from sidebar.
4959 if ( settings.is( ':visible' ) ) {
4960 document.getElementById( 'frm_insert_fields_tab' ).click();
4961 }
4962
4963 moveOpenModalsOutOfFieldOptions( settings );
4964 settings.remove();
4965
4966 $thisField.fadeOut( 'slow', function() {
4967 let $section = $thisField.closest( '.start_divider' ),
4968 type = $thisField.data( 'type' ),
4969 $adjacentFields = $thisField.siblings( 'li.form-field' ),
4970 $liWrapper;
4971
4972 if ( ! $adjacentFields.length ) {
4973 if ( $thisField.is( '.edit_field_type_end_divider' ) ) {
4974 $adjacentFields.length = $thisField.closest( 'li.form-field' ).siblings();
4975 } else {
4976 $liWrapper = $thisField.closest( 'ul.frm_sorting' ).parent();
4977 }
4978 }
4979
4980 $thisField.remove();
4981 if ( type === 'break' ) {
4982 renumberPageBreaks();
4983 } else if ( type === 'product' ) {
4984 maybeHideQuantityProductFieldOption();
4985 // a product field attached to a quantity field earlier might be the one deleted, so re-populate
4986 popAllProductFields();
4987 }
4988
4989 if ( $adjacentFields.length ) {
4990 syncLayoutClasses( $adjacentFields.first() );
4991 } else {
4992 $liWrapper.remove();
4993 }
4994
4995 if ( jQuery( '#frm-show-fields li' ).length === 0 || hasOnlySubmitField() ) {
4996 const formEditorContainer = document.getElementById( 'frm_form_editor_container' );
4997 formEditorContainer.classList.remove( 'frm-has-fields' );
4998 formEditorContainer.classList.add( 'frm-empty-fields' );
4999 } else if ( $section.length ) {
5000 toggleOneSectionHolder( $section );
5001 }
5002
5003 // prevent "More Options" tooltips from staying around after their target field is deleted.
5004 deleteTooltips();
5005 });
5006
5007 if ( $thisField.length ) {
5008 wp.hooks.doAction( 'frm_after_delete_field', $thisField[0] );
5009 }
5010 }
5011 });
5012 }
5013
5014 function addFieldLogicRow() {
5015 /*jshint validthis:true */
5016 const id = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' ),
5017 formId = thisFormId,
5018 logicRows = document.getElementById( 'frm_logic_row_' + id ).querySelectorAll( '.frm_logic_row' );
5019 jQuery.ajax({
5020 type: 'POST',
5021 url: ajaxurl,
5022 data: {
5023 action: 'frm_add_logic_row',
5024 form_id: formId,
5025 field_id: id,
5026 nonce: frmGlobal.nonce,
5027 meta_name: getNewRowId( logicRows, 'frm_logic_' + id + '_' ),
5028 fields: getFieldList()
5029 },
5030 success: function( html ) {
5031 jQuery( document.getElementById( 'logic_' + id ) ).fadeOut( 'slow', function() {
5032 const logicRow = jQuery( document.getElementById( 'frm_logic_row_' + id ) );
5033 logicRow.append( html );
5034 logicRow.closest( '.frm_logic_rows' ).fadeIn( 'slow' );
5035 });
5036 }
5037 });
5038 return false;
5039 }
5040
5041 function getNewRowId( rows, replace, defaultValue ) {
5042 if ( ! rows.length ) {
5043 return 'undefined' !== typeof defaultValue ? defaultValue : 0;
5044 }
5045 return parseInt( rows[ rows.length - 1 ].id.replace( replace, '' ), 10 ) + 1;
5046 }
5047
5048 function addWatchLookupRow() {
5049 /*jshint validthis:true */
5050 let lastRowId,
5051 id = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' ),
5052 formId = thisFormId,
5053 lookupBlockRows = document.getElementById( 'frm_watch_lookup_block_' + id ).children;
5054 jQuery.ajax({
5055 type: 'POST',
5056 url: ajaxurl,
5057 data: {
5058 action: 'frm_add_watch_lookup_row',
5059 form_id: formId,
5060 field_id: id,
5061 row_key: getNewRowId( lookupBlockRows, 'frm_watch_lookup_' + id + '_' ),
5062 nonce: frmGlobal.nonce
5063 },
5064 success: function( newRow ) {
5065 const watchRowBlock = jQuery( document.getElementById( 'frm_watch_lookup_block_' + id ) );
5066 watchRowBlock.append( newRow );
5067 watchRowBlock.fadeIn( 'slow' );
5068 }
5069 });
5070 return false;
5071 }
5072
5073 function resetOptionTextDetails() {
5074 jQuery( '.frm-single-settings ul input[type="text"][name^="field_options[options_"]' ).filter( '[data-value-on-load]' ).removeAttr( 'data-value-on-load' );
5075 jQuery( 'input[type="hidden"][name^=optionmap]' ).remove();
5076 }
5077
5078 function optionTextAlreadyExists( input ) {
5079 let fieldId = jQuery( input ).closest( '.frm-single-settings' ).attr( 'data-fid' ),
5080 optionInputs = jQuery( input ).closest( 'ul' ).get( 0 ).querySelectorAll( '.field_' + fieldId + '_option' ),
5081 index,
5082 optionInput;
5083
5084 for ( index in optionInputs ) {
5085 optionInput = optionInputs[ index ];
5086 if ( optionInput.id !== input.id && optionInput.value === input.value && optionInput.getAttribute( 'data-duplicate' ) !== 'true' ) {
5087 return true;
5088 }
5089 }
5090
5091 return false;
5092 }
5093
5094 function onOptionTextFocus() {
5095 let input,
5096 fieldId;
5097
5098 if ( this.getAttribute( 'data-value-on-load' ) === null ) {
5099 this.setAttribute( 'data-value-on-load', this.value );
5100
5101 fieldId = jQuery( this ).closest( '.frm-single-settings' ).attr( 'data-fid' );
5102 input = document.createElement( 'input' );
5103 input.value = this.value;
5104 input.setAttribute( 'type', 'hidden' );
5105 input.setAttribute( 'name', 'optionmap[' + fieldId + '][' + this.value + ']' );
5106 this.parentNode.appendChild( input );
5107
5108 if ( typeof optionMap[ fieldId ] === 'undefined' ) {
5109 optionMap[ fieldId ] = {};
5110 }
5111
5112 optionMap[ fieldId ][ this.value ] = input;
5113 }
5114
5115 if ( this.getAttribute( 'data-duplicate' ) === 'true' ) {
5116 this.removeAttribute( 'data-duplicate' );
5117
5118 // we want to use original value if actually still a duplicate
5119 if ( optionTextAlreadyExists( this ) ) {
5120 this.setAttribute( 'data-value-on-focus', this.getAttribute( 'data-value-on-load' ) );
5121 return;
5122 }
5123 }
5124
5125 if ( '' !== this.value || frmAdminJs.new_option !== this.getAttribute( 'data-value-on-focus' ) ) {
5126 this.setAttribute( 'data-value-on-focus', this.value );
5127 }
5128 }
5129
5130 /**
5131 * Returns an object that has the old and new values and labels, when a field choice is changed.
5132 *
5133 * @param {HTMLElement} input
5134 * @returns {Object}
5135 */
5136 function getChoiceOldAndNewValues( input ) {
5137 const { oldValue, oldLabel } = getChoiceOldValueAndLabel( input );
5138 const { newValue, newLabel } = getChoiceNewValueAndLabel( input );
5139
5140 return { oldValue, oldLabel, newValue, newLabel };
5141 }
5142
5143 /**
5144 * Returns an object that has the new value and label, when a field choice is changed.
5145 *
5146 * @param {HTMLElement} choiceElement
5147 * @returns {Object}
5148 */
5149 function getChoiceNewValueAndLabel( choiceElement ) {
5150 const singleOptionContainer = choiceElement.closest( '.frm_single_option' );
5151
5152 let newValue, newLabel;
5153
5154 if ( choiceElement.parentElement.classList.contains( 'frm_single_option' ) ) { // label changed
5155 newValue = singleOptionContainer.querySelector( '.frm_option_key input[type="text"]' ).value;
5156 newLabel = choiceElement.value;
5157 return { newValue, newLabel };
5158 }
5159
5160 // saved value changed
5161 newLabel = singleOptionContainer.querySelector( 'input[type="text"]' ).value;
5162 newValue = choiceElement.value;
5163 return { newValue, newLabel };
5164 }
5165
5166 /**
5167 * Returns an object that has the old value and label, when a field choice is changed.
5168 *
5169 * @param {HTMLElement} choiceElement
5170 * @returns {Object}
5171 */
5172 function getChoiceOldValueAndLabel( choiceElement ) {
5173 const usingSeparateValues = choiceElement.closest( '.frm-single-settings' ).querySelector( '.frm_toggle_sep_values' )?.checked ?? false;
5174 const singleOptionContainer = choiceElement.closest( '.frm_single_option' );
5175
5176 let oldValue, oldLabel;
5177
5178 if ( usingSeparateValues ) {
5179 if ( choiceElement.parentElement.classList.contains( 'frm_single_option' ) ) { // label changed
5180 oldValue = singleOptionContainer.querySelector( '.frm_option_key input[type="text"]' ).getAttribute( 'data-value-on-focus' );
5181 oldLabel = choiceElement.getAttribute( 'data-value-on-focus' );
5182 return { oldValue, oldLabel };
5183 }
5184 }
5185 oldValue = choiceElement.getAttribute( 'data-value-on-focus' );
5186 oldLabel = singleOptionContainer.querySelector( 'input[type="text"]' ).getAttribute( 'data-value-on-focus' );
5187
5188 return { oldValue, oldLabel };
5189 }
5190
5191 function onOptionTextBlur() {
5192 let originalValue,
5193 fieldId,
5194 fieldIndex,
5195 logicId,
5196 row,
5197 rowLength,
5198 rowIndex,
5199 valueSelect,
5200 opts,
5201 fieldIds,
5202 settingId,
5203 setting,
5204 optionMatches,
5205 option;
5206
5207 const { oldValue, oldLabel, newValue, newLabel } = getChoiceOldAndNewValues( this );
5208
5209 if ( oldValue === newValue && oldLabel === newLabel ) {
5210 return;
5211 }
5212
5213 const singleSettingsContainer = this.closest( '.frm-single-settings' );
5214
5215 fieldId = singleSettingsContainer.getAttribute( 'data-fid' );
5216 originalValue = this.getAttribute( 'data-value-on-load' );
5217
5218 // check if the newValue is already mapped to another option
5219 // if it is, mark as duplicate and return
5220 if ( optionTextAlreadyExists( this ) ) {
5221 this.setAttribute( 'data-duplicate', 'true' );
5222
5223 if ( typeof optionMap[ fieldId ] !== 'undefined' && typeof optionMap[ fieldId ][ originalValue ] !== 'undefined' ) {
5224 // unmap any other change that may have happened before instead of changing it to something unused
5225 optionMap[ fieldId ][ originalValue ].value = originalValue;
5226 }
5227
5228 return;
5229 }
5230
5231 if ( typeof optionMap[ fieldId ] !== 'undefined' && typeof optionMap[ fieldId ][ originalValue ] !== 'undefined' ) {
5232 optionMap[ fieldId ][ originalValue ].value = newValue;
5233 }
5234
5235 fieldIds = [];
5236 rows = builderPage.querySelectorAll( '.frm_logic_row' );
5237 rowLength = rows.length;
5238 for ( rowIndex = 0; rowIndex < rowLength; rowIndex++ ) {
5239 row = rows[ rowIndex ];
5240 opts = row.querySelector( '.frm_logic_field_opts' );
5241
5242 if ( opts.value !== fieldId ) {
5243 continue;
5244 }
5245
5246 logicId = row.id.split( '_' )[ 2 ];
5247 valueSelect = row.querySelector( 'select[name="field_options[hide_opt_' + logicId + '][]"]' );
5248
5249 if ( '' === oldValue ) {
5250 optionMatches = [];
5251 } else {
5252 optionMatches = valueSelect.querySelectorAll( 'option[value="' + oldValue + '"]' );
5253 }
5254
5255 if ( ! optionMatches.length ) {
5256 optionMatches = valueSelect.querySelectorAll( 'option[value="' + newValue + '"]' );
5257
5258 if ( ! optionMatches.length ) {
5259 if ( ! singleSettingsContainer.querySelector( '.frm_toggle_sep_values' )?.checked ) {
5260 option = searchSelectByText( valueSelect, oldValue ); // Find conditional logic option with oldValue
5261 }
5262
5263 if ( ! option ) {
5264 option = document.createElement( 'option' );
5265 valueSelect.appendChild( option );
5266 }
5267 }
5268 }
5269
5270 if ( optionMatches.length ) {
5271 option = optionMatches[ optionMatches.length - 1 ];
5272 }
5273
5274 option.setAttribute( 'value', newValue );
5275 option.textContent = newLabel;
5276
5277 if ( fieldIds.indexOf( logicId ) === -1 ) {
5278 fieldIds.push( logicId );
5279 }
5280 }
5281
5282 for ( fieldIndex in fieldIds ) {
5283 settingId = fieldIds[ fieldIndex ];
5284 setting = document.getElementById( 'frm-single-settings-' + settingId );
5285 moveFieldSettings( setting );
5286 }
5287 }
5288
5289 /**
5290 * Returns an option element that matches a string with its text content.
5291 *
5292 * @param {HTMLElement} selectElement
5293 * @param {string} searchText
5294 * @returns {HTMLElement|null}
5295 */
5296 function searchSelectByText( selectElement, searchText ) {
5297 const options = selectElement.options;
5298
5299 for ( let i = 0; i < options.length; i++ ) {
5300 const option = options[i];
5301 if ( searchText === option.textContent ) {
5302 return option;
5303 }
5304 }
5305
5306 return null;
5307 }
5308
5309 function updateGetValueFieldSelection() {
5310 /*jshint validthis:true */
5311 const fieldID = this.id.replace( 'get_values_form_', '' );
5312 const fieldSelect = document.getElementById( 'get_values_field_' + fieldID );
5313 const fieldType = this.getAttribute( 'data-fieldtype' );
5314
5315 if ( this.value === '' ) {
5316 fieldSelect.options.length = 1;
5317 } else {
5318 const formID = this.value;
5319 jQuery.ajax({
5320 type: 'POST', url: ajaxurl,
5321 data: {
5322 action: 'frm_get_options_for_get_values_field',
5323 form_id: formID,
5324 field_type: fieldType,
5325 nonce: frmGlobal.nonce
5326 },
5327 success: function( fields ) {
5328 fieldSelect.innerHTML = fields;
5329 }
5330 });
5331 }
5332 }
5333
5334 // Clear the Watch Fields option when Lookup field switches to "Text" option
5335 function maybeClearWatchFields() {
5336 /*jshint validthis:true */
5337 let link, lookupBlock,
5338 fieldID = this.name.replace( 'field_options[data_type_', '' ).replace( ']', '' );
5339
5340 link = document.getElementById( 'frm_add_watch_lookup_link_' + fieldID );
5341 if ( ! link ) {
5342 return;
5343 }
5344 link = link.parentNode;
5345
5346 if ( this.value === 'text' ) {
5347 lookupBlock = document.getElementById( 'frm_watch_lookup_block_' + fieldID );
5348 if ( lookupBlock !== null ) {
5349 // Clear and hide the Watch Fields option
5350 lookupBlock.innerHTML = '';
5351 link.classList.add( 'frm_hidden' );
5352
5353 // Hide the Watch Fields row
5354 link.previousElementSibling.style.display = 'none';
5355 link.previousElementSibling.previousElementSibling.style.display = 'none';
5356 link.previousElementSibling.previousElementSibling.previousElementSibling.style.display = 'none';
5357 }
5358 } else {
5359 // Show the Watch Fields option
5360 link.classList.remove( 'frm_hidden' );
5361 }
5362
5363 toggleMultiSelect( fieldID, this.value );
5364 }
5365
5366 // Number the pages and hide/show the first page as needed.
5367 function renumberPageBreaks() {
5368 let i, containerClass,
5369 pages = document.getElementsByClassName( 'frm-page-num' );
5370
5371 if ( pages.length > 1 ) {
5372 document.getElementById( 'frm-fake-page' ).style.display = 'block';
5373 for ( i = 0; i < pages.length; i++ ) {
5374 containerClass = pages[i].parentNode.parentNode.parentNode.classList;
5375 if ( i === 1 ) {
5376 // Hide previous button on page 1
5377 containerClass.add( 'frm-first-page' );
5378 } else {
5379 containerClass.remove( 'frm-first-page' );
5380 }
5381 pages[i].textContent = ( i + 1 );
5382 }
5383 } else {
5384 document.getElementById( 'frm-fake-page' ).style.display = 'none';
5385 }
5386
5387 wp.hooks.doAction( 'frm_renumber_page_breaks', pages );
5388 }
5389
5390 // The fake field works differently than real fields.
5391 function maybeCollapsePage() {
5392 /*jshint validthis:true */
5393 const field = jQuery( this ).closest( '.frm_field_box[data-ftype=break]' );
5394 if ( field.length ) {
5395 toggleCollapsePage( field );
5396 } else {
5397 toggleCollapseFakePage();
5398 }
5399 }
5400
5401 // Find all fields in a page and hide/show them
5402 function toggleCollapsePage( field ) {
5403 const toCollapse = getAllFieldsForPage( field.get( 0 ).parentNode.closest( 'li.frm_field_box' ).nextElementSibling );
5404 togglePage( field, toCollapse );
5405 }
5406
5407 function toggleCollapseFakePage() {
5408 const topLevel = document.getElementById( 'frm-fake-page' ),
5409 firstField = document.getElementById( 'frm-show-fields' ).firstElementChild,
5410 toCollapse = getAllFieldsForPage( firstField );
5411
5412 if ( firstField.getAttribute( 'data-ftype' ) === 'break' ) {
5413 // Don't collapse if the first field is a page break.
5414 return;
5415 }
5416
5417 togglePage( jQuery( topLevel ), toCollapse );
5418 }
5419
5420 function getAllFieldsForPage( firstWrapper ) {
5421 let $fieldsForPage, currentWrapper;
5422
5423 $fieldsForPage = jQuery();
5424
5425 if ( null === firstWrapper ) {
5426 return $fieldsForPage;
5427 }
5428
5429 currentWrapper = firstWrapper;
5430
5431 do {
5432 if ( null !== currentWrapper.querySelector( '.edit_field_type_break' ) ) {
5433 break;
5434 }
5435 $fieldsForPage = $fieldsForPage.add( jQuery( currentWrapper ) );
5436 currentWrapper = currentWrapper.nextElementSibling;
5437 } while ( null !== currentWrapper );
5438
5439 return $fieldsForPage;
5440 }
5441
5442 function togglePage( field, toCollapse ) {
5443 let i,
5444 fieldCount = toCollapse.length,
5445 slide = Math.min( fieldCount, 3 );
5446
5447 if ( field.hasClass( 'frm-page-collapsed' ) ) {
5448 field.removeClass( 'frm-page-collapsed' );
5449 toCollapse.removeClass( 'frm-is-collapsed' );
5450 for ( i = 0; i < slide; i++ ) {
5451 if ( i === slide - 1 ) {
5452 jQuery( toCollapse[ i ]).slideDown( 150, function() {
5453 toCollapse.show();
5454 });
5455 } else {
5456 jQuery( toCollapse[ i ]).slideDown( 150 );
5457 }
5458 }
5459 } else {
5460 field.addClass( 'frm-page-collapsed' );
5461 toCollapse.addClass( 'frm-is-collapsed' );
5462 for ( i = 0; i < slide; i++ ) {
5463 if ( i === slide - 1 ) {
5464 jQuery( toCollapse[ i ]).slideUp( 150, function() {
5465 toCollapse.css( 'cssText', 'display:none !important;' );
5466 });
5467 } else {
5468 jQuery( toCollapse[ i ]).slideUp( 150 );
5469 }
5470 }
5471 }
5472 }
5473
5474 function maybeCollapseSection() {
5475 /*jshint validthis:true */
5476 const parentCont = this.parentNode.parentNode.parentNode.parentNode;
5477
5478 parentCont.classList.toggle( 'frm-section-collapsed' );
5479 }
5480
5481 function maybeCollapseSettings() {
5482 /*jshint validthis:true */
5483 this.classList.toggle( 'frm-collapsed' );
5484
5485 // Toggles the "aria-expanded" attribute
5486 const expanded = this.getAttribute( 'aria-expanded' ) === 'true' || false;
5487 this.setAttribute( 'aria-expanded', ! expanded );
5488 }
5489
5490 function clickLabel() {
5491 if ( ! this.id ) {
5492 return;
5493 }
5494
5495 /*jshint validthis:true */
5496 let setting = document.querySelectorAll( '[data-changeme="' + this.id + '"]' )[0],
5497 fieldId = this.id.replace( 'field_label_', '' ),
5498 fieldType = document.getElementById( 'field_options_type_' + fieldId ),
5499 fieldTypeName = fieldType.value;
5500
5501 if ( typeof setting !== 'undefined' ) {
5502 if ( fieldType.tagName === 'SELECT' ) {
5503 fieldTypeName = fieldType.options[ fieldType.selectedIndex ].text.toLowerCase();
5504 } else {
5505 fieldTypeName = fieldTypeName.replace( '_', ' ' );
5506 }
5507
5508 fieldTypeName = normalizeFieldName( fieldTypeName );
5509
5510 setTimeout( function() {
5511 if ( setting.value.toLowerCase() === fieldTypeName ) {
5512 setting.select();
5513 } else {
5514 setting.focus();
5515 }
5516 }, 50 );
5517 }
5518 }
5519
5520 function clickDescription() {
5521 /*jshint validthis:true */
5522 const setting = document.querySelectorAll( '[data-changeme="' + this.id + '"]' )[0];
5523 if ( typeof setting !== 'undefined' ) {
5524 setTimeout( function() {
5525 setting.focus();
5526 autoExpandSettings( setting );
5527 }, 50 );
5528 }
5529 }
5530
5531 function autoExpandSettings( setting ) {
5532 const inSection = setting.closest( '.frm-collapse-me' );
5533 if ( inSection !== null ) {
5534 inSection.previousElementSibling.classList.remove( 'frm-collapsed' );
5535 }
5536 }
5537
5538 function normalizeFieldName( fieldTypeName ) {
5539 if ( fieldTypeName === 'divider' ) {
5540 fieldTypeName = 'section';
5541 } else if ( fieldTypeName === 'range' ) {
5542 fieldTypeName = 'slider';
5543 } else if ( fieldTypeName === 'data' ) {
5544 fieldTypeName = 'dynamic';
5545 } else if ( fieldTypeName === 'form' ) {
5546 fieldTypeName = 'embed form';
5547 }
5548 return fieldTypeName;
5549 }
5550
5551 function clickVis( e ) {
5552 /*jshint validthis:true */
5553 let currentClass, originalList;
5554
5555 currentClass = e.target.classList;
5556
5557 if ( currentClass.contains( 'frm-collapse-page' ) || currentClass.contains( 'frm-sub-label' ) || e.target.closest( '.dropdown' ) !== null ) {
5558 return;
5559 }
5560
5561 if ( this.closest( '.start_divider' ) !== null ) {
5562 e.stopPropagation();
5563 }
5564
5565 if ( this.classList.contains( 'edit_field_type_divider' ) ) {
5566 originalList = e.originalEvent.target.closest( 'ul.frm_sorting' );
5567 if ( null !== originalList ) {
5568 // prevent section click if clicking a field group within a section.
5569 if ( originalList.classList.contains( 'edit_field_type_divider' ) || originalList.parentNode.parentNode.classList.contains( 'start_divider' ) ) {
5570 return;
5571 }
5572 }
5573 }
5574
5575 clickAction( this );
5576 }
5577
5578 /**
5579 * Update the format input based on the selected format type.
5580 *
5581 * @since 6.9
5582 *
5583 * @param {Event} event The event object from the format type selection.
5584 * @return {void}
5585 */
5586 function maybeUpdateFormatInput( event ) {
5587 const formatElement = event.target;
5588 const type = formatElement.value
5589
5590 if ( 'custom' === type ) {
5591 const fieldId = formatElement.dataset.fieldId;
5592 const formatInput = document.getElementById( `frm-field-format-custom-${fieldId}` ).querySelector( '.frm_format_opt' );
5593
5594 if ( 'international' === formatInput.value || 'currency' === formatInput.value || 'number' === formatInput.value ) {
5595 formatInput.setAttribute( 'value', '' );
5596 }
5597 }
5598
5599 setTimeout(
5600 () => {
5601 formatElement.querySelectorAll( 'option' ).forEach(
5602 option => {
5603 if ( option.selected && option.classList.contains( 'frm_show_upgrade' ) ) {
5604 formatElement.value = 'none';
5605 }
5606 }
5607 );
5608 },
5609 0
5610 );
5611 }
5612
5613 /**
5614 * Open Advanced settings on double click.
5615 */
5616 function openAdvanced() {
5617 const fieldId = this.getAttribute( 'data-fid' );
5618 autoExpandSettings( document.getElementById( 'field_options_field_key_' + fieldId ) );
5619 }
5620
5621 function toggleRepeatButtons() {
5622 /*jshint validthis:true */
5623 const $thisField = jQuery( this ).closest( '.frm_field_box' );
5624 $thisField.find( '.repeat_icon_links' ).removeClass( 'repeat_format repeat_formatboth repeat_formattext' ).addClass( 'repeat_format' + this.value );
5625 if ( this.value === 'text' || this.value === 'both' ) {
5626 $thisField.find( '.frm_repeat_text' ).show();
5627 $thisField.find( '.repeat_icon_links a' ).addClass( 'frm_button' );
5628 } else {
5629 $thisField.find( '.frm_repeat_text' ).hide();
5630 $thisField.find( '.repeat_icon_links a' ).removeClass( 'frm_button' );
5631 }
5632 }
5633
5634 function checkRepeatLimit() {
5635 /*jshint validthis:true */
5636 const val = this.value;
5637 if ( val !== '' && ( val < 2 || val > 200 ) ) {
5638 infoModal( frmAdminJs.repeat_limit_min );
5639 this.value = '';
5640 }
5641 }
5642
5643 function checkCheckboxSelectionsLimit() {
5644 /*jshint validthis:true */
5645 const val = this.value;
5646 if ( val !== '' && ( val < 1 || val > 200 ) ) {
5647 infoModal( frmAdminJs.checkbox_limit );
5648 this.value = '';
5649 }
5650 }
5651
5652 function updateRepeatText( obj, addRemove ) {
5653 const $thisField = jQuery( obj ).closest( '.frm_field_box' );
5654 $thisField.find( '.frm_' + addRemove + '_form_row .frm_repeat_label' ).text( obj.value );
5655 }
5656
5657 function fieldsInSection( id ) {
5658 const children = [];
5659 jQuery( document.getElementById( 'frm_field_id_' + id ) ).find( 'li.frm_field_box:not(.no_repeat_section .edit_field_type_end_divider)' ).each( function() {
5660 children.push( jQuery( this ).data( 'fid' ) );
5661 });
5662 return children;
5663 }
5664
5665 function toggleFormTax() {
5666 /*jshint validthis:true */
5667 const id = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' );
5668 const val = this.value;
5669 const $showFields = document.getElementById( 'frm_show_selected_fields_' + id );
5670 const $showForms = document.getElementById( 'frm_show_selected_forms_' + id );
5671
5672 jQuery( $showForms ).find( 'select' ).val( '' );
5673 if ( val === 'form' ) {
5674 $showForms.style.display = 'inline';
5675 empty( $showFields );
5676 } else {
5677 $showFields.style.display = 'none';
5678 $showForms.style.display = 'none';
5679 getTaxOrFieldSelection( val, id );
5680 }
5681
5682 }
5683
5684 function resetOptOnChange() {
5685 /*jshint validthis:true */
5686 let field, thisOpt;
5687
5688 field = getFieldKeyFromOpt( this );
5689 if ( ! field ) {
5690 return;
5691 }
5692
5693 thisOpt = jQuery( this ).closest( '.frm_single_option' );
5694
5695 resetSingleOpt( field.fieldId, field.fieldKey, thisOpt );
5696 }
5697
5698 function getFieldKeyFromOpt( object ) {
5699 let allOpts, fieldId, fieldKey;
5700
5701 allOpts = jQuery( object ).closest( '.frm_sortable_field_opts' );
5702 if ( ! allOpts.length ) {
5703 return false;
5704 }
5705
5706 fieldId = allOpts.attr( 'id' ).replace( 'frm_field_', '' ).replace( '_opts', '' );
5707 fieldKey = allOpts.data( 'key' );
5708
5709 return {
5710 fieldId: fieldId,
5711 fieldKey: fieldKey
5712 };
5713 }
5714
5715 function resetSingleOpt( fieldId, fieldKey, thisOpt ) {
5716 let saved, text, defaultVal, previewInput, labelForDisplay, optContainer,
5717 optKey = thisOpt.data( 'optkey' ),
5718 separateValues = usingSeparateValues( fieldId ),
5719 single = jQuery( 'label[for="field_' + fieldKey + '-' + optKey + '"]' ),
5720 baseName = 'field_options[options_' + fieldId + '][' + optKey + ']',
5721 label = jQuery( 'input[name="' + baseName + '[label]"]' );
5722
5723 if ( single.length < 1 ) {
5724 resetDisplayedOpts( fieldId );
5725
5726 // Set the default value.
5727 defaultVal = thisOpt.find( 'input[name^="default_value_"]' );
5728 if ( defaultVal.is( ':checked' ) && label.length > 0 ) {
5729 jQuery( 'select[name^="item_meta[' + fieldId + ']"]' ).val( label.val() );
5730 }
5731 return;
5732 }
5733
5734 previewInput = single.children( 'input' );
5735
5736 if ( label.length < 1 ) {
5737 // Check for other label.
5738 label = jQuery( 'input[name="' + baseName + '"]' );
5739 saved = label.val();
5740 } else if ( separateValues ) {
5741 saved = jQuery( 'input[name="' + baseName + '[value]"]' ).val();
5742 } else {
5743 saved = label.val();
5744 }
5745
5746 if ( label.length < 1 ) {
5747 return;
5748 }
5749
5750 // Set the displayed value.
5751 text = single[0].childNodes;
5752
5753 if ( imagesAsOptions( fieldId ) ) {
5754 labelForDisplay = getImageDisplayValue( thisOpt, fieldId, label );
5755 optContainer = single.find( '.frm_image_option_container' );
5756
5757 if ( optContainer.length > 0 ) {
5758 optContainer.replaceWith( labelForDisplay );
5759 } else {
5760 text[ text.length - 1 ].nodeValue = '';
5761 single.append( labelForDisplay );
5762 }
5763 } else {
5764 let firstInputIndex = false;
5765 text.forEach( ( node, index ) => {
5766 if ( firstInputIndex === false ) {
5767 if ( node.tagName === 'INPUT' ) {
5768 firstInputIndex = index;
5769 }
5770 } else if ( index === firstInputIndex + 1 ) {
5771 let nodeValue = '';
5772
5773 if ( buttonsAsOptions( fieldId ) ) {
5774 nodeValue = div({ className: 'frm_label_button_container', text: ' ' + label.val() });
5775 single[0].replaceChild( nodeValue, node );
5776 } else {
5777 node.nodeValue = ' ' + label.val();
5778 }
5779 } else {
5780 single[0].removeChild( node );
5781 }
5782 });
5783 }
5784
5785 // Set saved value.
5786 previewInput.val( saved );
5787
5788 // Set the default value.
5789 defaultVal = thisOpt.find( 'input[name^="default_value_"]' );
5790 previewInput.prop( 'checked', defaultVal.is( ':checked' ) ? true : false );
5791 }
5792
5793 function buttonsAsOptions( fieldId ) {
5794 const fields = document.getElementsByName( 'field_options[image_options_' + fieldId + ']' );
5795 const result = Array.from( fields ).find( field => field.checked && ( 'buttons' === field.value ) );
5796
5797 return typeof result !== 'undefined';
5798 }
5799
5800 /**
5801 * Set the displayed value for an image option.
5802 */
5803 function getImageDisplayValue( thisOpt, fieldId, label ) {
5804 let image, imageUrl, showLabelWithImage, fieldType;
5805
5806 image = thisOpt.find( 'img' );
5807 if ( image ) {
5808 imageUrl = image.attr( 'src' );
5809 }
5810
5811 showLabelWithImage = showingLabelWithImage( fieldId );
5812 fieldType = radioOrCheckbox( fieldId );
5813 return getImageLabel( label.val(), showLabelWithImage, imageUrl, fieldType );
5814 }
5815
5816 function getImageOptionSize( fieldId ) {
5817 let val,
5818 field = document.getElementById( 'field_options_image_size_' + fieldId ),
5819 size = '';
5820
5821 if ( field !== null ) {
5822 val = field.value;
5823 if ( val !== '' ) {
5824 size = val;
5825 }
5826 }
5827
5828 return size;
5829 }
5830
5831 function resetDisplayedOpts( fieldId ) {
5832 let i, opts, type, placeholder, fieldInfo,
5833 input = jQuery( '[name^="item_meta[' + fieldId + ']"]' );
5834
5835 if ( input.length < 1 ) {
5836 return;
5837 }
5838
5839 if ( input.is( 'select' ) ) {
5840 placeholder = document.getElementById( 'frm_placeholder_' + fieldId );
5841 if ( placeholder !== null && placeholder.value === '' ) {
5842 fillDropdownOpts( input[0], { sourceID: fieldId });
5843 } else {
5844 fillDropdownOpts( input[0], {
5845 sourceID: fieldId,
5846 placeholder: placeholder.value
5847 });
5848 }
5849 } else {
5850 opts = getMultipleOpts( fieldId );
5851 jQuery( '#field_' + fieldId + '_inner_container > .frm_form_fields' ).html( '' );
5852 fieldInfo = getFieldKeyFromOpt( jQuery( '#frm_delete_field_' + fieldId + '-000_container' ) );
5853
5854 const container = jQuery( '#field_' + fieldId + '_inner_container > .frm_form_fields' ),
5855 hasImageOptions = imagesAsOptions( fieldId ),
5856 imageSize = hasImageOptions ? getImageOptionSize( fieldId ) : '',
5857 imageOptionClass = hasImageOptions ? ( 'frm_image_option frm_image_' + imageSize + ' ' ) : '',
5858 isProduct = isProductField( fieldId );
5859
5860 type = ( 'hidden' === input.attr( 'type' ) ? input.data( 'field-type' ) : input.attr( 'type' ) );
5861 for ( i = 0; i < opts.length; i++ ) {
5862 container.append( addRadioCheckboxOpt( type, opts[ i ], fieldId, fieldInfo.fieldKey, isProduct, imageOptionClass ) );
5863 }
5864 }
5865
5866 adjustConditionalLogicOptionOrders( fieldId );
5867 }
5868
5869 /**
5870 * Returns an object that has a value and label for new conditional logic option, for a given option value.
5871 *
5872 * @param {Number} fieldId
5873 * @param {string} expectedOption
5874 * @returns {Object}
5875 */
5876 function getNewConditionalLogicOption( fieldId, expectedOption ) {
5877 const optionsContainer = document.getElementById( 'frm_field_' + fieldId + '_opts' );
5878
5879 const expectedOptionInput = optionsContainer.querySelector( 'input[value="' + expectedOption + '"]' );
5880
5881 if ( expectedOptionInput ) {
5882 return getChoiceNewValueAndLabel( expectedOptionInput );
5883 }
5884
5885 return { newValue: expectedOption, newLabel: expectedOption };
5886 }
5887
5888 function adjustConditionalLogicOptionOrders( fieldId, type ) {
5889 let row, opts, logicId, valueSelect, optionLength, optionIndex, expectedOption, optionMatch, fieldOptions,
5890 rows = builderPage.querySelectorAll( '.frm_logic_row' ),
5891 rowLength = rows.length;
5892
5893 fieldOptions = wp.hooks.applyFilters( 'frm_conditional_logic_field_options', getFieldOptions( fieldId ), { type, fieldId });
5894 optionLength = fieldOptions.length;
5895
5896 for ( rowIndex = 0; rowIndex < rowLength; rowIndex++ ) {
5897 row = rows[ rowIndex ];
5898 opts = row.querySelector( '.frm_logic_field_opts' );
5899
5900 if ( opts.value != fieldId ) {
5901 continue;
5902 }
5903
5904 logicId = row.id.split( '_' )[ 2 ];
5905 valueSelect = row.querySelector( 'select[name="field_options[hide_opt_' + logicId + '][]"]' );
5906
5907 for ( optionIndex = optionLength - 1; optionIndex >= 0; optionIndex-- ) {
5908 expectedOption = fieldOptions[ optionIndex ];
5909 let expectedOptionValue = document.getElementById( 'frm_field_' + fieldId + '_opts' ).querySelector( '.frm_option_key input[type="text"]' )?.value;
5910 if ( ! expectedOptionValue ) {
5911 expectedOptionValue = expectedOption;
5912 }
5913
5914 optionMatch = valueSelect.querySelector( 'option[value="' + expectedOptionValue + '"]' );
5915
5916 const { newValue, newLabel } = getNewConditionalLogicOption( fieldId, expectedOption );
5917
5918 const fieldChoices = document.querySelectorAll( '#frm_field_' + fieldId + '_opts input[data-value-on-focus]' );
5919 const expectedChoiceEl = Array.from( fieldChoices ).find( element => element.value === expectedOption );
5920 if ( expectedChoiceEl ) {
5921 const oldValue = expectedChoiceEl.dataset.valueOnFocus;
5922 const hasMatch = oldValue && valueSelect.querySelector( 'option[value="' + oldValue + '"]' );
5923 if ( hasMatch ) {
5924 continue;
5925 }
5926 }
5927 prependValueSelectWithOptionMatch( valueSelect, optionMatch, newValue, newLabel );
5928 }
5929
5930 optionMatch = valueSelect.querySelector( 'option[value=""]' );
5931 if ( optionMatch !== null ) {
5932 valueSelect.prepend( optionMatch );
5933 }
5934 }
5935 }
5936
5937 function prependValueSelectWithOptionMatch( valueSelect, optionMatch, newValue, newLabel ) {
5938 if ( optionMatch === null && ! valueSelect.querySelector( 'option[value="' + newValue + '"]' )) {
5939 optionMatch = frmDom.tag( 'option', { text: newLabel });
5940 optionMatch.value = newValue;
5941 }
5942
5943 valueSelect.prepend( optionMatch );
5944 }
5945
5946 function getFieldOptions( fieldId ) {
5947 let index, input, li, listItems, optsContainer, length,
5948 options = [];
5949 optsContainer = document.getElementById( 'frm_field_' + fieldId + '_opts' );
5950
5951 if ( ! optsContainer ) {
5952 return options;
5953 }
5954 listItems = optsContainer.querySelectorAll( '.frm_single_option' );
5955 length = listItems.length;
5956
5957 for ( index = 0; index < length; index++ ) {
5958 li = listItems[ index ];
5959
5960 if ( li.classList.contains( 'frm_hidden' ) ) {
5961 continue;
5962 }
5963
5964 input = li.querySelector( '.field_' + fieldId + '_option' );
5965 options.push( input.value );
5966 }
5967 return options;
5968 }
5969
5970 function addRadioCheckboxOpt( type, opt, fieldId, fieldKey, isProduct, classes ) {
5971 let other,
5972 single = '',
5973 isOther = opt.key.indexOf( 'other' ) !== -1,
5974 id = 'field_' + fieldKey + '-' + opt.key,
5975 inputType = type === 'scale' ? 'radio' : type;
5976
5977 other = '<input type="text" id="field_' + fieldKey + '-' + opt.key + '-otext" class="frm_other_input frm_pos_none" name="item_meta[other][' + fieldId + '][' + opt.key + ']" value="" />';
5978
5979 this.getSingle = function() {
5980
5981 /**
5982 * Get single option template.
5983 * @param {Object} option Object containing the option data.
5984 * @param {string} type The field type.
5985 * @param {string} fieldId The field id.
5986 * @param {string} classes The option clasnames.
5987 * @param {string} id The input id attribute.
5988 */
5989 single = wp.hooks.applyFilters( 'frm_admin.build_single_option_template', single, { opt, type, fieldId, classes, id });
5990
5991 if ( '' !== single ) {
5992 return single;
5993 }
5994
5995 return '<div class="frm_' + type + ' ' + type + ' ' + classes + '" id="frm_' + type + '_' + fieldId + '-' + opt.key + '"><label for="' + id +
5996 '"><input type="' + inputType +
5997 '" name="item_meta[' + fieldId + ']' + ( type === 'checkbox' ? '[]' : '' ) +
5998 '" value="' + purifyHtml( opt.saved ) + '" id="' + id + '"' + ( isProduct ? ' data-price="' + opt.price + '"' : '' ) + ( opt.checked ? ' checked="checked"' : '' ) + '> ' + purifyHtml( opt.label ) + '</label>' +
5999 ( isOther ? other : '' ) +
6000 '</div>';
6001 };
6002
6003 return this.getSingle();
6004 }
6005
6006 function fillDropdownOpts( field, atts ) {
6007 if ( field === null ) {
6008 return;
6009 }
6010 const sourceID = atts.sourceID,
6011 placeholder = atts.placeholder,
6012 isProduct = isProductField( sourceID ),
6013 showOther = atts.other;
6014
6015 removeDropdownOpts( field );
6016 let opts = getMultipleOpts( sourceID ),
6017 hasPlaceholder = ( typeof placeholder !== 'undefined' );
6018
6019 for ( let i = 0; i < opts.length; i++ ) {
6020 let label = opts[ i ].label,
6021 isOther = opts[ i ].key.indexOf( 'other' ) !== -1;
6022
6023 if ( hasPlaceholder && label !== '' ) {
6024 addBlankSelectOption( field, placeholder );
6025 } else if ( hasPlaceholder ) {
6026 label = placeholder;
6027 }
6028 hasPlaceholder = false;
6029
6030 if ( ! isOther || showOther ) {
6031 const opt = document.createElement( 'option' );
6032 opt.value = opts[ i ].saved;
6033 opt.innerHTML = purifyHtml( label );
6034
6035 if ( isProduct ) {
6036 opt.setAttribute( 'data-price', opts[ i ].price );
6037 }
6038
6039 field.appendChild( opt );
6040 }
6041 }
6042 }
6043
6044 function addBlankSelectOption( field, placeholder ) {
6045 const opt = document.createElement( 'option' ),
6046 firstChild = field.firstChild;
6047
6048 opt.value = '';
6049 opt.innerHTML = placeholder;
6050 if ( firstChild !== null ) {
6051 field.insertBefore( opt, firstChild );
6052 field.selectedIndex = 0;
6053 } else {
6054 field.appendChild( opt );
6055 }
6056 }
6057
6058 function getMultipleOpts( fieldId ) {
6059 let i, saved, labelName, label, key, optObj,
6060 fieldType,
6061 checked = false,
6062 opts = [],
6063 imageUrl = '';
6064
6065 const optVals = jQuery( 'input[name^="field_options[options_' + fieldId + ']"]' );
6066 const isProduct = isProductField( fieldId );
6067 const showLabelWithImage = showingLabelWithImage( fieldId );
6068 const hasImageOptions = imagesAsOptions( fieldId );
6069 const separateValues = usingSeparateValues( fieldId );
6070
6071 for ( i = 0; i < optVals.length; i++ ) {
6072 if ( optVals[ i ].name.indexOf( '[000]' ) > 0 || optVals[ i ].name.indexOf( '[value]' ) > 0 || optVals[ i ].name.indexOf( '[image]' ) > 0 || optVals[ i ].name.indexOf( '[price]' ) > 0 ) {
6073 continue;
6074 }
6075
6076 saved = optVals[ i ].value;
6077 label = saved;
6078 key = optVals[ i ].name.replace( 'field_options[options_' + fieldId + '][', '' ).replace( '[label]', '' ).replace( ']', '' );
6079
6080 if ( separateValues ) {
6081 labelName = optVals[ i ].name.replace( '[label]', '[value]' );
6082 saved = jQuery( 'input[name="' + labelName + '"]' ).val();
6083 }
6084
6085 if ( hasImageOptions ) {
6086 imageUrl = getImageUrlFromInput( optVals[i]);
6087 fieldType = radioOrCheckbox( fieldId );
6088 label = getImageLabel( label, showLabelWithImage, imageUrl, fieldType );
6089 }
6090
6091 /**
6092 * @since 5.0.04
6093 */
6094 label = frmAdminBuild.hooks.applyFilters( 'frm_choice_field_label', label, fieldId, optVals[ i ], hasImageOptions );
6095
6096 checked = getChecked( optVals[ i ].id );
6097
6098 optObj = {
6099 saved: saved,
6100 label: label,
6101 checked: checked,
6102 key: key
6103 };
6104
6105 if ( isProduct ) {
6106 labelName = optVals[ i ].name.replace( '[label]', '[price]' );
6107 optObj.price = jQuery( 'input[name="' + labelName + '"]' ).val();
6108 }
6109
6110 opts.push( optObj );
6111 }
6112
6113 return opts;
6114 }
6115
6116 function radioOrCheckbox( fieldId ) {
6117 const settings = document.getElementById( 'frm-single-settings-' + fieldId );
6118 if ( settings === null ) {
6119 return 'radio';
6120 }
6121
6122 return settings.classList.contains( 'frm-type-checkbox' ) ? 'checkbox' : 'radio';
6123 }
6124
6125 function getImageUrlFromInput( optVal ) {
6126 let img,
6127 wrapper = jQuery( optVal ).siblings( '.frm_image_preview_wrapper' );
6128
6129 if ( ! wrapper.length ) {
6130 return '';
6131 }
6132
6133 img = wrapper.find( 'img' );
6134 if ( ! img.length ) {
6135 return '';
6136 }
6137
6138 return img.attr( 'src' );
6139 }
6140
6141 function purifyHtml( html ) {
6142 if ( html instanceof Element || html instanceof Document ) {
6143 html = html.outerHTML;
6144 }
6145
6146 const clean = jQuery.parseHTML( html ).reduce(
6147 ( total, currentNode ) => {
6148 const cleanNode = frmDom.cleanNode( currentNode );
6149
6150 if ( '#text' === cleanNode.nodeName ) {
6151 return total += cleanNode.textContent;
6152 }
6153
6154 return total + cleanNode.outerHTML;
6155 },
6156 ''
6157 );
6158
6159 if ( clean !== html ) {
6160 // Clean it until nothing changes, in case the stripped result is now unsafe.
6161 return purifyHtml( clean );
6162 }
6163
6164 return clean;
6165 }
6166
6167 function getImageLabel( label, showLabelWithImage, imageUrl, fieldType ) {
6168 let imageLabelClass,
6169 originalLabel = label,
6170 shape = fieldType === 'checkbox' ? 'square' : 'circle',
6171 labelImage,
6172 labelNode,
6173 imageLabel;
6174
6175 originalLabel = purifyHtml( originalLabel );
6176
6177 if ( imageUrl ) {
6178 labelImage = img({ src: imageUrl, alt: originalLabel });
6179 } else {
6180 labelImage = div({ className: 'frm_empty_url' });
6181 labelImage.innerHTML = frmAdminJs.image_placeholder_icon;
6182 }
6183
6184 imageLabelClass = showLabelWithImage ? ' frm_label_with_image' : '';
6185
6186 imageLabel = tag( 'span', { className: 'frm_text_label_for_image_inner' });
6187
6188 imageLabel.innerHTML = originalLabel;
6189 labelNode = tag(
6190 'span',
6191 {
6192 className: 'frm_image_option_container' + imageLabelClass,
6193 children: [
6194 labelImage,
6195 tag( 'span', { className: 'frm_text_label_for_image', child: imageLabel })
6196 ]
6197 }
6198 );
6199
6200 return labelNode;
6201 }
6202
6203 function getChecked( id ) {
6204 field = jQuery( '#' + id );
6205
6206 if ( field.length === 0 ) {
6207 return false;
6208 }
6209
6210 checkbox = field.siblings( 'input[type=checkbox]' );
6211
6212 return checkbox.length && checkbox.prop( 'checked' );
6213 }
6214
6215 function removeDropdownOpts( field ) {
6216 let i;
6217 if ( typeof field.options === 'undefined' ) {
6218 return;
6219 }
6220
6221 for ( i = field.options.length - 1; i >= 0; i-- ) {
6222 field.remove( i );
6223 }
6224 }
6225
6226 /**
6227 * Is the box checked to use separate values?
6228 */
6229 function usingSeparateValues( fieldId ) {
6230 return isChecked( 'separate_value_' + fieldId );
6231 }
6232
6233 /**
6234 * Is the box checked to use images as options?
6235 */
6236 function imagesAsOptions( fieldId ) {
6237 let checked = false,
6238 field = document.getElementsByName( 'field_options[image_options_' + fieldId + ']' );
6239
6240 for ( let i = 0; i < field.length; i++ ) {
6241 if ( field[ i ].checked ) {
6242 checked = '0' !== field[ i ].value;
6243 }
6244 }
6245
6246 /**
6247 * @since 5.0.04
6248 */
6249 return frmAdminBuild.hooks.applyFilters( 'frm_choice_field_images_as_options', checked, fieldId );
6250 }
6251
6252 function showingLabelWithImage( fieldId ) {
6253 const isShowing = ! isChecked( 'hide_image_text_' + fieldId );
6254
6255 /**
6256 * @since 5.0.04
6257 */
6258 return frmAdminBuild.hooks.applyFilters( 'frm_choice_field_showing_label_with_image', isShowing, fieldId );
6259 }
6260
6261 function isChecked( id ) {
6262 const field = document.getElementById( id );
6263 if ( field === null ) {
6264 return false;
6265 }
6266 return field.checked;
6267 }
6268
6269 function checkUniqueOpt( targetInput ) {
6270 const settingsContainer = targetInput.closest( '.frm-single-settings' );
6271 const fieldId = settingsContainer.getAttribute( 'data-fid' );
6272 const areValuesSeparate = settingsContainer.querySelector( '[name="field_options[separate_value_' + fieldId + ']"]' ).checked;
6273
6274 if ( areValuesSeparate && ! targetInput.name.endsWith( '[value]' ) ) {
6275 return;
6276 }
6277
6278 const container = document.getElementById( 'frm_field_' + fieldId + '_opts' );
6279 const conflicts = Array.from( container.querySelectorAll( 'input[type="text"]' ) ).filter(
6280 input => input.id !== targetInput.id &&
6281 areValuesSeparate === input.name.endsWith( '[value]' ) &&
6282 input.value === targetInput.value
6283 );
6284
6285 if ( conflicts.length ) {
6286 /* translators: %s: The detected option value. */
6287 infoModal( sprintf( __( 'Duplicate option value "%s" detected', 'formidable' ), purifyHtml( targetInput.value ) ) );
6288 }
6289 }
6290
6291 function getFieldValues() {
6292 /*jshint validthis:true */
6293 let isTaxonomy,
6294 val = this.value;
6295
6296 if ( val ) {
6297 const parentIDs = this.parentNode.id.replace( 'frm_logic_', '' ).split( '_' );
6298 const fieldID = parentIDs[0];
6299 const metaKey = parentIDs[1];
6300 const valueField = document.getElementById( 'frm_field_id_' + val );
6301 const valueFieldType = valueField.getAttribute( 'data-ftype' );
6302 const fill = document.getElementById( 'frm_show_selected_values_' + fieldID + '_' + metaKey );
6303 const optionName = 'field_options[hide_opt_' + fieldID + '][]';
6304 const optionID = 'frm_field_logic_opt_' + fieldID;
6305 let input = false;
6306 let showSelect = ( valueFieldType === 'select' || valueFieldType === 'checkbox' || valueFieldType === 'radio' );
6307 const showText = ( valueFieldType === 'text' || valueFieldType === 'email' || valueFieldType === 'phone' || valueFieldType === 'url' || valueFieldType === 'number' );
6308
6309 if ( showSelect ) {
6310 isTaxonomy = document.getElementById( 'frm_has_hidden_options_' + val );
6311 if ( isTaxonomy !== null ) {
6312 // get the category options with ajax
6313 showSelect = false;
6314 }
6315 }
6316
6317 if ( showSelect || showText ) {
6318 const comparison = document.querySelector( `#frm_logic_${fieldID}_${metaKey} [name="field_options[hide_field_cond_${fieldID}][]"]` ).value;
6319 fill.innerHTML = '';
6320 const creatingValuesDropdown = showSelect && ! [ 'LIKE', 'not LIKE', 'LIKE%', '%LIKE' ].includes( comparison );
6321 if ( creatingValuesDropdown ) {
6322 input = document.createElement( 'select' );
6323 } else {
6324 input = document.createElement( 'input' );
6325 input.type = 'text';
6326 }
6327 input.name = optionName;
6328 input.id = optionID + '_' + metaKey;
6329 fill.appendChild( input );
6330
6331 if ( creatingValuesDropdown ) {
6332 const fillField = document.getElementById( input.id );
6333 fillDropdownOpts( fillField, {
6334 sourceID: val,
6335 placeholder: '',
6336 other: true
6337 });
6338 }
6339 } else {
6340 const thisType = this.getAttribute( 'data-type' );
6341 const callback = () => {
6342 const event = new CustomEvent( 'frm_logic_options_loaded' );
6343 event.frmData = { valueFieldType, fieldID, metaKey };
6344 document.dispatchEvent( event );
6345 };
6346
6347 frmGetFieldValues( val, fieldID, metaKey, thisType, undefined, callback );
6348 }
6349 }
6350 }
6351
6352 function getFieldSelection() {
6353 /*jshint validthis:true */
6354 const formId = this.value;
6355 if ( formId ) {
6356 const fieldId = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' );
6357 getTaxOrFieldSelection( formId, fieldId );
6358 }
6359 }
6360
6361 function getTaxOrFieldSelection( formId, fieldId ) {
6362 if ( formId ) {
6363 jQuery.ajax({
6364 type: 'POST',
6365 url: ajaxurl,
6366 data: {
6367 action: 'frm_get_field_selection',
6368 field_id: fieldId,
6369 form_id: formId,
6370 nonce: frmGlobal.nonce
6371 },
6372 success: function( msg ) {
6373 jQuery( '#frm_show_selected_fields_' + fieldId ).html( msg ).show();
6374 }
6375 });
6376 }
6377 }
6378
6379 function updateFieldOrder() {
6380 let self = this;
6381
6382 this.initOnceInAllInstances = function() {
6383 if ( 'undefined' !== typeof updateFieldOrder.prototype.orderFieldsObject ) {
6384 return;
6385 }
6386
6387 // It will store the order input fields ( input[name="field_options[field_order_{fieldId}]"] ).
6388 // It will help to reduce the DOM searches based on fieldId.
6389 // The same object data is used across all "updateFieldOrder" instances.
6390 updateFieldOrder.prototype.orderFieldsObject = {};
6391
6392 // Get the Form group that will handle the fields settings.
6393 // Perform a single DOM search and use it across all "updateFieldOrder" instances.
6394 updateFieldOrder.prototype.fieldSettingsForm = document.getElementById( 'frm-end-form-marker' ).closest( 'form' );
6395 };
6396
6397 this.getFieldOrderInputById = function( fieldId, parent ) {
6398 let field;
6399 const orderFieldsObject = updateFieldOrder.prototype.orderFieldsObject;
6400 const fieldSettingsForm = updateFieldOrder.prototype.fieldSettingsForm;
6401
6402 if ( 'undefined' === typeof orderFieldsObject[ fieldId ]) {
6403 field = fieldSettingsForm.querySelector( 'input[name="field_options[field_order_' + fieldId + ']"]' );
6404 if ( null === field ) {
6405 field = parent.querySelector( 'input[name="field_options[field_order_' + fieldId + ']"]' );
6406 }
6407 orderFieldsObject[ fieldId ] = field;
6408 return field;
6409 }
6410
6411 return orderFieldsObject[ fieldId ];
6412 };
6413
6414 this.initOnceInAllInstances();
6415 renumberPageBreaks();
6416
6417 return ( function() {
6418 let fieldId, field, currentOrder, newOrder,
6419 moveFieldsClass = new moveFieldSettings(),
6420 fields = jQuery( 'li.frm_field_box', jQuery( '#frm-show-fields' ) );
6421
6422 for ( i = 0; i < fields.length; i++ ) {
6423 fieldId = fields[ i ].getAttribute( 'data-fid' );
6424 field = self.getFieldOrderInputById( fieldId, fields[ i ]);
6425
6426 // get current field order, make sure we don't get the "field" reference as the "field" value will get updated later.
6427 currentOrder = null !== field ? Object.assign({}, field.value )[0] : null;
6428 newOrder = i + 1;
6429
6430 if ( currentOrder != newOrder && null !== currentOrder ) {
6431 field.value = newOrder;
6432 singleField = fields[ i ].querySelector( '#frm-single-settings-' + fieldId );
6433
6434 // add field that needs to be moved to "updateFieldOrder.prototype.fieldSettingsForm"
6435 moveFieldsClass.append( singleField );
6436 fieldUpdated();
6437 }
6438 }
6439 // move all appended fields
6440 moveFieldsClass.moveFields();
6441 }() );
6442 }
6443
6444 function toggleSectionHolder() {
6445 document.querySelectorAll( '.start_divider' ).forEach(
6446 function( divider ) {
6447 toggleOneSectionHolder( jQuery( divider ) );
6448 }
6449 );
6450 }
6451
6452 function toggleOneSectionHolder( $section ) {
6453 let noSectionFields, $rows, length, index, sectionHasFields;
6454
6455 if ( ! $section.length ) {
6456 return;
6457 }
6458
6459 $rows = $section.find( 'ul.frm_sorting' );
6460 sectionHasFields = false;
6461 length = $rows.length;
6462 for ( index = 0; index < length; ++index ) {
6463 if ( 0 !== getFieldsInRow( jQuery( $rows.get( index ) ) ).length ) {
6464 sectionHasFields = true;
6465 break;
6466 }
6467 }
6468
6469 noSectionFields = $section.parent().children( '.frm_no_section_fields' ).get( 0 );
6470 noSectionFields.classList.toggle( 'frm_block', ! sectionHasFields );
6471 }
6472
6473 function handleShowPasswordLiveUpdate() {
6474 frmDom.util.documentOn( 'change', '.frm_show_password_setting_input', event => {
6475 const fieldId = event.target.getAttribute( 'data-fid' );
6476 const fieldEl = document.getElementById( 'frm_field_id_' + fieldId );
6477 if ( ! fieldEl ) {
6478 return;
6479 }
6480
6481 fieldEl.classList.toggle( 'frm_disabled_show_password', ! event.target.checked );
6482 });
6483 }
6484
6485 function slideDown() {
6486 /*jshint validthis:true */
6487 const id = jQuery( this ).data( 'slidedown' );
6488 const $thisId = jQuery( document.getElementById( id ) );
6489 if ( $thisId.is( ':hidden' ) ) {
6490 $thisId.slideDown( 'fast' );
6491 this.style.display = 'none';
6492 }
6493 return false;
6494 }
6495
6496 function slideUp() {
6497 /*jshint validthis:true */
6498 const id = jQuery( this ).data( 'slideup' );
6499 const $thisId = jQuery( document.getElementById( id ) );
6500 $thisId.slideUp( 'fast' );
6501 $thisId.siblings( 'a' ).show();
6502 return false;
6503 }
6504
6505 function adjustVisibilityValuesForEveryoneValues( element, option ) {
6506 if ( '' === option.getAttribute( 'value' ) ) {
6507 onEveryoneOptionSelected( jQuery( this ) );
6508 } else {
6509 unselectEveryoneOptionIfSelected( jQuery( this ) );
6510 }
6511 }
6512
6513 function onEveryoneOptionSelected( $select ) {
6514 $select.val( '' );
6515 $select.next( '.btn-group' ).find( '.multiselect-container input[value!=""]' ).prop( 'checked', false );
6516 }
6517
6518 function unselectEveryoneOptionIfSelected( $select ) {
6519 let selectedValues = $select.val(),
6520 index;
6521
6522 if ( selectedValues === null ) {
6523 $select.next( '.btn-group' ).find( '.multiselect-container input[value=""]' ).prop( 'checked', true );
6524 onEveryoneOptionSelected( $select );
6525 return;
6526 }
6527
6528 index = selectedValues.indexOf( '' );
6529 if ( index >= 0 ) {
6530 selectedValues.splice( index, 1 );
6531 $select.val( selectedValues );
6532 $select.next( '.btn-group' ).find( '.multiselect-container input[value=""]' ).prop( 'checked', false );
6533 }
6534 }
6535
6536 /**
6537 * Get rid of empty container that inserts extra space.
6538 */
6539 function hideEmptyEle() {
6540 jQuery( '.frm-hide-empty' ).each( function() {
6541 if ( jQuery( this ).text().trim().length === 0 ) {
6542 jQuery( this ).remove();
6543 }
6544 });
6545 }
6546
6547 /* Change the classes in the builder */
6548 function changeFieldClass( field, setting ) {
6549 let classes, replace, alignField,
6550 replaceWith = ' ' + setting.value,
6551 fieldId = field.getAttribute( 'data-fid' );
6552
6553 // Include classes from multiple settings.
6554 if ( typeof fieldId !== 'undefined' ) {
6555 if ( setting.classList.contains( 'field_options_align' ) ) {
6556 replaceWith += ' ' + document.getElementById( 'frm_classes_' + fieldId ).value;
6557 } else if ( setting.classList.contains( 'frm_classes' ) ) {
6558 alignField = document.getElementById( 'field_options_align_' + fieldId );
6559 if ( alignField !== null ) {
6560 replaceWith += ' ' + alignField.value;
6561 }
6562 }
6563 }
6564 replaceWith += ' ';
6565
6566 // Allow for the column number dropdown.
6567 replaceWith = replaceWith.replace( ' block ', ' ' ).replace( ' inline ', ' horizontal_radio ' );
6568
6569 classes = field.className.split( ' frmstart ' )[1];
6570 classes = 0 === classes.indexOf( 'frmend ' ) ? '' : classes.split( ' frmend ' )[0];
6571
6572 if ( classes.trim() === '' ) {
6573 replace = ' frmstart frmend ';
6574 if ( -1 === field.className.indexOf( replace ) ) {
6575 replace = ' frmstart frmend ';
6576 }
6577 replaceWith = ' frmstart ' + replaceWith.trim() + ' frmend ';
6578 } else {
6579 replace = classes.trim();
6580 replaceWith = replaceWith.trim();
6581 }
6582
6583 field.className = field.className.replace( replace, replaceWith );
6584 }
6585
6586 function maybeShowInlineModal( e ) {
6587 /*jshint validthis:true */
6588 e.preventDefault();
6589 showInlineModal( this );
6590 }
6591
6592 function showInlineModal( icon, input ) {
6593 const box = document.getElementById( icon.getAttribute( 'data-open' ) ),
6594 container = jQuery( icon ).closest( 'p' ),
6595 inputTrigger = ( typeof input !== 'undefined' );
6596
6597 if ( container.hasClass( 'frm-open' ) ) {
6598 container.removeClass( 'frm-open' );
6599 box.classList.add( 'frm_hidden' );
6600 } else {
6601 if ( ! inputTrigger ) {
6602 input = getInputForIcon( icon );
6603 }
6604 if ( input !== null ) {
6605 if ( ! inputTrigger ) {
6606 input.focus();
6607 }
6608 container.after( box );
6609 box.setAttribute( 'data-fills', input.id );
6610
6611 if ( box.id.indexOf( 'frm-calc-box' ) === 0 ) {
6612 popCalcFields( box, true );
6613 }
6614 }
6615
6616 container.addClass( 'frm-open' );
6617 box.classList.remove( 'frm_hidden' );
6618
6619 /**
6620 * @since 6.4.1
6621 */
6622 wp.hooks.doAction( 'frm_show_inline_modal', box, icon );
6623 }
6624 }
6625
6626 function dismissInlineModal( e ) {
6627 /*jshint validthis:true */
6628 e.preventDefault();
6629 this.parentNode.classList.add( 'frm_hidden' );
6630 jQuery( '.frm-open [data-open="' + this.parentNode.id + '"]' ).closest( '.frm-open' ).removeClass( 'frm-open' );
6631 }
6632
6633 function changeInputtedValue() {
6634 /*jshint validthis:true */
6635 let i,
6636 action = this.getAttribute( 'data-frmchange' ).split( ',' );
6637
6638 for ( i = 0; i < action.length; i++ ) {
6639 if ( action[i] === 'updateOption' ) {
6640 changeHiddenSeparateValue( this );
6641 } else if ( action[i] === 'updateDefault' ) {
6642 changeDefaultRadioValue( this );
6643 } else if ( action[i] === 'checkUniqueOpt' ) {
6644 checkUniqueOpt( this );
6645 } else {
6646 this.value = this.value[ action[i] ]();
6647 }
6648 }
6649 }
6650
6651 /**
6652 * When the saved value is changed, update the default value radio.
6653 */
6654 function changeDefaultRadioValue( input ) {
6655 const parentLi = getOptionParent( input ),
6656 key = parentLi.getAttribute( 'data-optkey' ),
6657 fieldId = getOptionFieldId( parentLi, key ),
6658 defaultRadio = parentLi.querySelector( 'input[name="default_value_' + fieldId + '"]' );
6659
6660 if ( defaultRadio !== null ) {
6661 defaultRadio.value = input.value;
6662 }
6663 }
6664
6665 /**
6666 * If separate values are not enabled, change the saved value when
6667 * the displayed value is changed.
6668 */
6669 function changeHiddenSeparateValue( input ) {
6670 let savedVal,
6671 parentLi = getOptionParent( input ),
6672 key = parentLi.getAttribute( 'data-optkey' ),
6673 fieldId = getOptionFieldId( parentLi, key ),
6674 sep = document.getElementById( 'separate_value_' + fieldId );
6675
6676 if ( sep !== null && sep.checked === false ) {
6677 // If separate values are not turned on.
6678 savedVal = document.getElementById( 'field_key_' + fieldId + '-' + key );
6679 savedVal.value = input.value;
6680 changeDefaultRadioValue( savedVal );
6681 }
6682 }
6683
6684 function getOptionParent( input ) {
6685 let parentLi = input.parentNode;
6686 if ( parentLi.tagName !== 'LI' ) {
6687 parentLi = parentLi.parentNode;
6688 }
6689 return parentLi;
6690 }
6691
6692 function getOptionFieldId( li, key ) {
6693 const liId = li.id;
6694
6695 return liId.replace( 'frm_delete_field_', '' ).replace( '-' + key + '_container', '' );
6696 }
6697
6698 function submitBuild() {
6699 /*jshint validthis:true */
6700 const $thisEle = this;
6701
6702 if ( showNameYourFormModal() ) {
6703 return;
6704 }
6705
6706 preFormSave( this );
6707
6708 const $form = jQuery( builderForm );
6709 const v = JSON.stringify( $form.serializeArray() );
6710
6711 jQuery( document.getElementById( 'frm_compact_fields' ) ).val( v );
6712 jQuery.ajax({
6713 type: 'POST',
6714 url: ajaxurl,
6715 data: {action: 'frm_save_form', 'frm_compact_fields': v, nonce: frmGlobal.nonce},
6716 success: function( msg ) {
6717 afterFormSave( $thisEle );
6718
6719 const $postStuff = document.getElementById( 'post-body-content' );
6720 const $html = document.createElement( 'div' );
6721 $html.setAttribute( 'class', 'frm_updated_message' );
6722 $html.innerHTML = msg;
6723 $postStuff.insertBefore( $html, $postStuff.firstChild );
6724 reloadIfAddonActivatedAjaxSubmitOnly();
6725 },
6726 error: function() {
6727 triggerSubmit( document.getElementById( 'frm_js_build_form' ) );
6728 }
6729 });
6730 }
6731
6732 function triggerSubmit( form ) {
6733 const button = form.ownerDocument.createElement( 'input' );
6734 button.style.display = 'none';
6735 button.type = 'submit';
6736 form.appendChild( button ).click();
6737 form.removeChild( button );
6738 }
6739
6740 function triggerChange( element ) {
6741 jQuery( element ).trigger( 'change' );
6742 }
6743
6744 function submitNoAjax() {
6745 /*jshint validthis:true */
6746 let form;
6747
6748 if ( showNameYourFormModal() ) {
6749 return;
6750 }
6751
6752 preFormSave( this );
6753 form = jQuery( builderForm );
6754 jQuery( document.getElementById( 'frm_compact_fields' ) ).val( JSON.stringify( form.serializeArray() ) );
6755 triggerSubmit( document.getElementById( 'frm_js_build_form' ) );
6756 }
6757
6758 /**
6759 * Display a modal dialog for naming a new form template, if applicable.
6760 *
6761 * @return {boolean} True if the modal is successfully initialized and displayed; false otherwise.
6762 */
6763 function showNameYourFormModal() {
6764 // Exit early if the 'new_template' URL parameter is not set to 'true'
6765 if ( ! shouldShowNameYourFormNameModal() ) {
6766 return false;
6767 }
6768
6769 const modalWidget = initModal( '#frm-form-templates-modal', '440px' );
6770 if ( ! modalWidget ) {
6771 return false;
6772 }
6773
6774 // Set the vertical offset for the modal and open it
6775 offsetModalY( modalWidget, '72px' );
6776 modalWidget.dialog( 'open' );
6777
6778 return true;
6779 }
6780
6781 /**
6782 * Returns true if 'Name Your Form' modal should be displayed.
6783 *
6784 * @returns {Boolean}
6785 */
6786 function shouldShowNameYourFormNameModal() {
6787 const formNameInput = document.getElementById( 'frm_form_name' );
6788 if ( formNameInput && formNameInput.value.trim() !== '' ) {
6789 return false;
6790 }
6791
6792 return 'true' === urlParams.get( 'new_template' ) && document.querySelector( '#frm_top_bar #frm_bs_dropdown .frm_bstooltip' )?.textContent.trim() === frm_admin_js.noTitleText; // eslint-disable-line camelcase
6793 }
6794
6795 /**
6796 * Manages event handling for the 'Name your form' modal.
6797 *
6798 * Attaches click and keydown event listeners to the save button and input field.
6799 *
6800 * @return {void}
6801 */
6802 function addFormNameModalEvents() {
6803 const saveFormNameButton = document.getElementById( 'frm-save-form-name-button' );
6804 const newFormNameInput = document.getElementById( 'frm_new_form_name_input' );
6805
6806 // Attach click event listener
6807 onClickPreventDefault( saveFormNameButton, onSaveFormNameButton );
6808
6809 // Attach keydown event listener
6810 newFormNameInput.addEventListener( 'keydown', function( event ) {
6811 if ( event.key === 'Enter' ) {
6812 onSaveFormNameButton.call( this, event );
6813 }
6814 });
6815 }
6816
6817 /**
6818 * Handles the click event on the save form name button.
6819 *
6820 * @param {Event} event The click event object.
6821 * @return {void}
6822 */
6823 const onSaveFormNameButton = ( event ) => {
6824 const newFormName = document.getElementById( 'frm_new_form_name_input' ).value.trim();
6825
6826 // Prepare FormData for the POST request
6827 const formData = new FormData();
6828 formData.append( 'form_id', urlParams.get( 'id' ) );
6829 formData.append( 'form_name', newFormName );
6830
6831 // Perform the POST request
6832 doJsonPost( 'rename_form', formData ).then( data => {
6833 // Remove the 'new_template' parameter from the URL and update the browser history
6834 urlParams.delete( 'new_template' );
6835 currentURL.search = urlParams.toString();
6836 history.replaceState({}, '', currentURL.toString() );
6837
6838 if ( null !== document.getElementById( 'frm_notification_settings' ) ) {
6839 document.getElementById( 'frm_form_name' ).value = newFormName;
6840 document.getElementById( 'frm_form_key' ).value = data.form_key;
6841 }
6842
6843 // Trigger the 'Save' button click using jQuery
6844 jQuery( '#frm-publishing' ).find( '.frm_button_submit' ).trigger( 'click' );
6845 });
6846 };
6847
6848 function preFormSave( b ) {
6849 removeWPUnload();
6850 if ( jQuery( 'form.inplace_form' ).length ) {
6851 jQuery( '.inplace_save, .postbox' ).trigger( 'click' );
6852 }
6853
6854 if ( b.classList.contains( 'frm_button_submit' ) ) {
6855 b.classList.add( 'frm_loading_form' );
6856 } else {
6857 b.classList.add( 'frm_loading_button' );
6858 }
6859 b.setAttribute( 'aria-busy', 'true' );
6860
6861 adjustFormatInputBeforeSave();
6862 }
6863
6864 /**
6865 * Updates the format input based on the selected format type from dropdowns during the form save process.
6866 *
6867 * @since 6.9
6868 *
6869 * @return {void}
6870 */
6871 function adjustFormatInputBeforeSave() {
6872 const formatTypes = document.querySelectorAll( '.frm_format_dropdown, .frm_phone_type_dropdown' );
6873 const valueMap = {
6874 none: '',
6875 international: 'international',
6876 currency: 'currency',
6877 number: 'number'
6878 };
6879
6880 formatTypes.forEach( formatType => {
6881 const value = formatType.value;
6882 if ( value in valueMap ) {
6883 const formatInput = document.getElementById( `frm_format_${formatType.dataset.fieldId}` );
6884 formatInput.value = valueMap[ value ];
6885 }
6886 });
6887 }
6888
6889 function afterFormSave( button ) {
6890 button.classList.remove( 'frm_loading_form' );
6891 button.classList.remove( 'frm_loading_button' );
6892 resetOptionTextDetails();
6893 fieldsUpdated = 0;
6894 button.setAttribute( 'aria-busy', 'false' );
6895
6896 setTimeout( function() {
6897 jQuery( '.frm_updated_message' ).fadeOut( 'slow', function() {
6898 this.parentNode.removeChild( this );
6899 });
6900 }, 5000 );
6901 }
6902
6903 function initUpgradeModal() {
6904 const $info = initModal( '#frm_upgrade_modal' );
6905 if ( $info === false ) {
6906 return;
6907 }
6908
6909 document.addEventListener( 'click', handleUpgradeClick );
6910 frmDom.util.documentOn( 'change', 'select.frm_select_with_upgrade', handleUpgradeClick );
6911
6912 function handleUpgradeClick( event ) {
6913 let element, link, content;
6914
6915 element = event.target;
6916
6917 if ( ! element.classList ) {
6918 return;
6919 }
6920
6921 const showExpiredModal = element.classList.contains( 'frm_show_expired_modal' ) || null !== element.querySelector( '.frm_show_expired_modal' ) || element.closest( '.frm_show_expired_modal' );
6922
6923 // If a `select` element is clicked, check if the selected option has a 'data-upgrade' attribute
6924 if ( event.type === 'change' && element.classList.contains( 'frm_select_with_upgrade' ) ) {
6925 const selectedOption = element.options[element.selectedIndex];
6926 if ( selectedOption && selectedOption.dataset.upgrade ) {
6927 element = selectedOption;
6928 }
6929 }
6930
6931 if ( ! element.dataset.upgrade ) {
6932 let parent = element.closest( '[data-upgrade]' );
6933 if ( ! parent ) {
6934 parent = element.closest( '.frm_field_box' );
6935 if ( ! parent ) {
6936 return;
6937 }
6938 // Fake it if it's missing to avoid error.
6939 element.dataset.upgrade = '';
6940 }
6941 element = parent;
6942 }
6943
6944 if ( showExpiredModal ) {
6945 const hookName = 'frm_show_expired_modal';
6946 wp.hooks.doAction( hookName, element );
6947 return;
6948 }
6949
6950 const upgradeLabel = element.dataset.upgrade;
6951 if ( ! upgradeLabel || element.classList.contains( 'frm_show_upgrade_tab' ) ) {
6952 return;
6953 }
6954
6955 event.preventDefault();
6956
6957 const modal = $info.get( 0 );
6958 const lockIcon = modal.querySelector( '.frm_lock_icon' );
6959
6960 if ( lockIcon ) {
6961 lockIcon.style.display = 'block';
6962 lockIcon.classList.remove( 'frm_lock_open_icon' );
6963 lockIcon.querySelector( 'use' ).setAttribute( 'href', '#frm_lock_icon' );
6964 }
6965
6966 const upgradeImageId = 'frm_upgrade_modal_image';
6967 const oldImage = document.getElementById( upgradeImageId );
6968 if ( oldImage ) {
6969 oldImage.remove();
6970 }
6971
6972 if ( element.dataset.image ) {
6973 if ( lockIcon ) {
6974 lockIcon.style.display = 'none';
6975 }
6976 lockIcon.parentNode.insertBefore( img({ id: upgradeImageId, src: frmGlobal.url + '/images/' + element.dataset.image }), lockIcon );
6977 }
6978
6979 const level = modal.querySelector( '.license-level' );
6980 if ( level ) {
6981 level.textContent = getRequiredLicenseFromTrigger( element );
6982 }
6983
6984 // If one click upgrade, hide other content
6985 addOneClick( element, 'modal', upgradeLabel );
6986
6987 modal.querySelector( '.frm_are_not_installed' ).style.display = element.dataset.image ? 'none' : 'inline-block';
6988 modal.querySelector( '.frm_feature_label' ).textContent = upgradeLabel;
6989 modal.querySelector( 'h2' ).style.display = 'block';
6990
6991 $info.dialog( 'open' );
6992
6993 // set the utm medium
6994 const button = modal.querySelector( '.button-primary:not(.frm-oneclick-button)' );
6995 link = button.getAttribute( 'href' ).replace( /(medium=)[a-z_-]+/ig, '$1' + element.getAttribute( 'data-medium' ) );
6996 content = element.getAttribute( 'data-content' );
6997 if ( content === null ) {
6998 content = '';
6999 }
7000 link = link.replace( /(content=)[a-z_-]+/ig, '$1' + content );
7001 button.setAttribute( 'href', link );
7002 }
7003 }
7004
7005 function getRequiredLicenseFromTrigger( element ) {
7006 if ( element.dataset.requires ) {
7007 return element.dataset.requires;
7008 }
7009 return 'Pro';
7010 }
7011
7012 function populateUpgradeTab( element ) {
7013 const title = element.dataset.upgrade;
7014
7015 const tab = element.getAttribute( 'href' ).replace( '#', '' );
7016 const container = document.querySelector( '.frm_' + tab ) || document.querySelector( '.' + tab );
7017
7018 if ( ! container ) {
7019 return;
7020 }
7021
7022 if ( container.querySelector( '.frm-upgrade-message' ) ) {
7023 // Tab has already been populated.
7024 return;
7025 }
7026
7027 const h2 = container.querySelector( 'h2' );
7028 h2.style.borderBottom = 'none';
7029
7030 /* translators: %s: Form Setting section name (ie Form Permissions, Form Scheduling). */
7031 h2.textContent = sprintf( __( '%s are not installed', 'formidable' ), title );
7032
7033 container.classList.add( 'frmcenter' );
7034
7035 const upgradeModal = document.getElementById( 'frm_upgrade_modal' );
7036 appendClonedModalElementToContainer( 'frm-oneclick' );
7037 appendClonedModalElementToContainer( 'frm-addon-status' );
7038
7039 // Borrow the call to action from the Upgrade upgradeModal which should exist on the settings page (it is still used for other upgrades including Actions).
7040 const upgradeModalLink = upgradeModal.querySelector( '.frm-upgrade-link' );
7041 if ( upgradeModalLink ) {
7042 const upgradeButton = upgradeModalLink.cloneNode( true );
7043 const level = upgradeButton.querySelector( '.license-level' );
7044
7045 if ( level ) {
7046 level.textContent = getRequiredLicenseFromTrigger( element );
7047 }
7048
7049 container.appendChild( upgradeButton );
7050
7051 // Maybe append the secondary "Already purchased?" link from the upgradeModal as well.
7052 if ( upgradeModalLink.nextElementSibling && upgradeModalLink.nextElementSibling.querySelector( '.frm-link-secondary' ) ) {
7053 container.appendChild( upgradeModalLink.nextElementSibling.cloneNode( true ) );
7054 }
7055
7056 appendClonedModalElementToContainer( 'frm-oneclick-button' );
7057 }
7058
7059 appendClonedModalElementToContainer( 'frm-upgrade-message' );
7060
7061 let upgradeLabel = element.dataset.message;
7062
7063 if ( upgradeLabel === undefined ) {
7064 upgradeLabel = element.dataset.upgrade;
7065 }
7066 addOneClick( element, 'tab', upgradeLabel );
7067
7068 if ( element.dataset.screenshot ) {
7069 container.appendChild( getScreenshotWrapper( element.dataset.screenshot ) );
7070 }
7071
7072 function appendClonedModalElementToContainer( className ) {
7073 container.appendChild( upgradeModal.querySelector( '.' + className ).cloneNode( true ) );
7074 }
7075 }
7076
7077 function getScreenshotWrapper( screenshot ) {
7078 const folderUrl = frmGlobal.url + '/images/screenshots/';
7079 const wrapper = div({
7080 className: 'frm-settings-screenshot-wrapper',
7081 children: [
7082 getToolbar(),
7083 div({ child: img({ src: folderUrl + screenshot }) })
7084 ]
7085 });
7086
7087 function getToolbar() {
7088 const children = getColorIcons();
7089 children.push( img({ src: frmGlobal.url + '/images/tab.svg' }) );
7090 return div({
7091 className: 'frm-settings-screenshot-toolbar',
7092 children
7093 });
7094 }
7095
7096 function getColorIcons() {
7097 return [ '#ED8181', '#EDE06A', '#80BE30' ].map(
7098 color => {
7099 const circle = div({ className: 'frm-minmax-icon' });
7100 circle.style.backgroundColor = color;
7101 return circle;
7102 }
7103 );
7104 }
7105
7106 return wrapper;
7107 }
7108
7109 /**
7110 * Allow addons to be installed from the upgrade modal.
7111 *
7112 * @param {Element} link
7113 * @param {String} context Either 'modal' or 'tab'.
7114 * @param {String|undefined} upgradeLabel
7115 */
7116 function addOneClick( link, context, upgradeLabel ) {
7117 let container;
7118
7119 if ( 'modal' === context ) {
7120 container = document.getElementById( 'frm_upgrade_modal' );
7121 } else if ( 'tab' === context ) {
7122 container = document.getElementById( link.getAttribute( 'href' ).substr( 1 ) );
7123 } else {
7124 return;
7125 }
7126
7127 const oneclickMessage = container.querySelector( '.frm-oneclick' );
7128 const upgradeMessage = container.querySelector( '.frm-upgrade-message' );
7129 const showLink = container.querySelector( '.frm-upgrade-link' );
7130 const button = container.querySelector( '.frm-oneclick-button' );
7131 const addonStatus = container.querySelector( '.frm-addon-status' );
7132
7133 let oneclick = link.getAttribute( 'data-oneclick' );
7134 let newMessage = link.getAttribute( 'data-message' );
7135 let showIt = 'block';
7136 let showMsg = 'block';
7137 let hideIt = 'none';
7138
7139 // If one click upgrade, hide other content.
7140 if ( oneclickMessage !== null && typeof oneclick !== 'undefined' && oneclick ) {
7141 if ( newMessage === null ) {
7142 showMsg = 'none';
7143 }
7144 showIt = 'none';
7145 hideIt = 'block';
7146 oneclick = JSON.parse( oneclick );
7147
7148 button.className = button.className.replace( ' frm-install-addon', '' ).replace( ' frm-activate-addon', '' );
7149 button.className = button.className + ' ' + oneclick.class;
7150 button.rel = oneclick.url;
7151
7152 if ( oneclick.class === 'frm-activate-addon' ) {
7153 oneclickMessage.textContent = __( 'This plugin is not activated. Would you like to activate it now?', 'formidable' );
7154 button.textContent = __( 'Activate', 'formidable' );
7155 } else {
7156 oneclickMessage.textContent = __( 'That add-on is not installed. Would you like to install it now?', 'formidable' );
7157 button.textContent = __( 'Install', 'formidable' );
7158 }
7159 }
7160
7161 if ( ! newMessage ) {
7162 newMessage = upgradeMessage.getAttribute( 'data-default' );
7163 }
7164 if ( undefined !== upgradeLabel ) {
7165 newMessage = newMessage.replace( '<span class="frm_feature_label"></span>', upgradeLabel );
7166 }
7167
7168 upgradeMessage.innerHTML = newMessage;
7169
7170 if ( link.dataset.upsellImage ) {
7171 upgradeMessage.appendChild(
7172 img({
7173 src: link.dataset.upsellImage,
7174 alt: link.dataset.upgrade
7175 })
7176 );
7177 }
7178
7179 // Either set the link or use the default.
7180 showLink.href = getShowLinkHrefValue( link, showLink );
7181
7182 addonStatus.style.display = 'none';
7183
7184 oneclickMessage.style.display = hideIt;
7185 button.style.display = hideIt === 'block' ? 'inline-block' : hideIt;
7186 upgradeMessage.style.display = showMsg;
7187 showLink.style.display = showIt === 'block' ? 'inline-block' : showIt;
7188 }
7189
7190 function getShowLinkHrefValue( link, showLink ) {
7191 let customLink = link.getAttribute( 'data-link' );
7192 if ( customLink === null || typeof customLink === 'undefined' || customLink === '' ) {
7193 customLink = showLink.getAttribute( 'data-default' );
7194 }
7195 return customLink;
7196 }
7197
7198 /* Form settings */
7199
7200 function showInputIcon( parentClass ) {
7201 if ( typeof parentClass === 'undefined' ) {
7202 parentClass = '';
7203 }
7204 maybeAddFieldSelection( parentClass );
7205 jQuery( parentClass + ' .frm_has_shortcodes:not(.frm-with-right-icon) input,' + parentClass + ' .frm_has_shortcodes:not(.frm-with-right-icon) textarea' ).wrap( '<span class="frm-with-right-icon"></span>' ).before( '<svg class="frmsvg frm-show-box"><use xlink:href="#frm_more_horiz_solid_icon"/></svg>' );
7206 }
7207
7208 /**
7209 * For reverse compatibility. Check for fields that were
7210 * using the old sidebar.
7211 */
7212 function maybeAddFieldSelection( parentClass ) {
7213 let i,
7214 missingClass = jQuery( parentClass + ' :not(.frm_has_shortcodes) .frm_not_email_message, ' + parentClass + ' :not(.frm_has_shortcodes) .frm_not_email_to, ' + parentClass + ' :not(.frm_has_shortcodes) .frm_not_email_subject' );
7215 for ( i = 0; i < missingClass.length; i++ ) {
7216 missingClass[i].parentNode.classList.add( 'frm_has_shortcodes' );
7217 }
7218 }
7219
7220 function showSuccessOpt() {
7221 /*jshint validthis:true */
7222 let c = 'success';
7223 if ( this.name === 'options[edit_action]' ) {
7224 c = 'edit';
7225 }
7226 const v = jQuery( this ).val();
7227 jQuery( '.' + c + '_action_box' ).hide();
7228 if ( v === 'redirect' ) {
7229 jQuery( '.' + c + '_action_redirect_box.' + c + '_action_box' ).fadeIn( 'slow' );
7230 } else if ( v === 'page' ) {
7231 jQuery( '.' + c + '_action_page_box.' + c + '_action_box' ).fadeIn( 'slow' );
7232 } else {
7233 jQuery( '.' + c + '_action_message_box.' + c + '_action_box' ).fadeIn( 'slow' );
7234 }
7235 }
7236
7237 function copyFormAction( event ) {
7238 if ( waitForActionToLoadBeforeCopy( event.target ) ) {
7239 return;
7240 }
7241
7242 const targetSettings = event.target.closest( '.frm_form_action_settings' );
7243 const wysiwygs = targetSettings.querySelectorAll( '.wp-editor-area' );
7244 if ( wysiwygs.length ) {
7245 // Temporary remove TinyMCE before cloning to avoid TinyMCE conflicts.
7246 wysiwygs.forEach( wysiwyg => {
7247 tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, wysiwyg.id );
7248 });
7249 }
7250
7251 const $action = jQuery( targetSettings ).clone();
7252 const currentID = $action.attr( 'id' ).replace( 'frm_form_action_', '' );
7253 const newID = newActionId( currentID );
7254
7255 $action.find( '.frm_action_id, .frm-btn-group' ).remove();
7256 $action.find( 'input[name$="[' + currentID + '][ID]"]' ).val( '' );
7257 $action.find( '.widget-inside' ).hide();
7258
7259 // the .html() gets original values, so they need to be set
7260 $action.find( 'input[type=text], textarea, input[type=number]' ).prop( 'defaultValue', function() {
7261 return this.value;
7262 });
7263
7264 $action.find( 'input[type=checkbox], input[type=radio]' ).prop( 'defaultChecked', function() {
7265 return this.checked;
7266 });
7267
7268 const rename = new RegExp( '\\[' + currentID + '\\]', 'g' );
7269 const reid = new RegExp( '_' + currentID + '"', 'g' );
7270 const reclass = new RegExp( '-' + currentID + '"', 'g' );
7271 const revalue = new RegExp( '"' + currentID + '"', 'g' ); // if a field id matches, this could cause trouble
7272
7273 let html = $action.html().replace( rename, '[' + newID + ']' ).replace( reid, '_' + newID + '"' );
7274 html = html.replace( reclass, '-' + newID + '"' ).replace( revalue, '"' + newID + '"' );
7275
7276 const newAction = div({
7277 id: 'frm_form_action_' + newID,
7278 className: $action.get( 0 ).className
7279 });
7280 newAction.setAttribute( 'data-actionkey', newID );
7281 newAction.innerHTML = html;
7282 newAction.querySelectorAll( '.wp-editor-wrap, .wp-editor-wrap *' ).forEach(
7283 element => {
7284 if ( 'string' === typeof element.className ) {
7285 element.className = element.className.replace( currentID, newID );
7286 }
7287 element.id = element.id.replace( currentID, newID );
7288 }
7289 );
7290 newAction.classList.remove( 'open' );
7291 document.getElementById( 'frm_notification_settings' ).appendChild( newAction );
7292
7293 if ( wysiwygs.length ) {
7294 // Re-initialize the original wysiwyg which was removed before cloning.
7295 wysiwygs.forEach( wysiwyg => {
7296 frmDom.wysiwyg.init( wysiwyg );
7297 });
7298
7299 newAction.querySelectorAll( '.wp-editor-area' ).forEach( wysiwyg => {
7300 frmDom.wysiwyg.init( wysiwyg );
7301 });
7302 }
7303
7304 if ( newAction.classList.contains( 'frm_single_on_submit_settings' ) ) {
7305 const autocompleteInput = newAction.querySelector( 'input.frm-page-search' );
7306 if ( autocompleteInput ) {
7307 initAutocomplete( newAction );
7308 }
7309 }
7310
7311 initiateMultiselect();
7312
7313 const hookName = 'frm_after_duplicate_action';
7314 wp.hooks.doAction( hookName, newAction );
7315 }
7316
7317 function waitForActionToLoadBeforeCopy( element ) {
7318 let $trigger = jQuery( element ),
7319 $original = $trigger.closest( '.frm_form_action_settings' ),
7320 $inside = $original.find( '.widget-inside' ),
7321 $top;
7322
7323 if ( $inside.find( 'p, div, table' ).length ) {
7324 return false;
7325 }
7326
7327 $top = $original.find( '.widget-top' );
7328 $top.on( 'frm-action-loaded', function() {
7329 $trigger.trigger( 'click' );
7330 $original.removeClass( 'open' );
7331 $inside.hide();
7332 });
7333 $top.trigger( 'click' );
7334 return true;
7335 }
7336
7337 function newActionId( currentID ) {
7338 let newID = parseInt( currentID, 10 ) + 11;
7339 const exists = document.getElementById( 'frm_form_action_' + newID );
7340 if ( exists !== null ) {
7341 newID++;
7342 newID = newActionId( newID );
7343 }
7344 return newID;
7345 }
7346
7347 function addFormAction() {
7348 /*jshint validthis:true */
7349 const type = jQuery( this ).data( 'actiontype' );
7350
7351 if ( isAtLimitForActionType( type ) ) {
7352 return;
7353 }
7354
7355 const actionId = getNewActionId();
7356 const formId = thisFormId;
7357
7358 const placeholderSetting = document.createElement( 'div' );
7359 placeholderSetting.classList.add( 'frm_single_' + type + '_settings' );
7360
7361 const actionsList = document.getElementById( 'frm_notification_settings' );
7362 actionsList.appendChild( placeholderSetting );
7363
7364 jQuery.ajax({
7365 type: 'POST',
7366 url: ajaxurl,
7367 data: {
7368 action: 'frm_add_form_action',
7369 type: type,
7370 list_id: actionId,
7371 form_id: formId,
7372 nonce: frmGlobal.nonce
7373 },
7374 success: handleAddFormActionSuccess
7375 });
7376
7377 function handleAddFormActionSuccess( html ) {
7378 fieldUpdated();
7379 placeholderSetting.remove();
7380
7381 closeOpenActions();
7382
7383 const newActionContainer = div();
7384 newActionContainer.innerHTML = html;
7385
7386 const widgetTop = newActionContainer.querySelector( '.widget-top' );
7387 Array.from( newActionContainer.children ).forEach( child => actionsList.appendChild( child ) );
7388
7389 jQuery( '.frm_form_action_settings' ).fadeIn( 'slow' );
7390
7391 const newAction = document.getElementById( 'frm_form_action_' + actionId );
7392
7393 newAction.classList.add( 'open' );
7394 document.getElementById( 'post-body-content' ).scroll({
7395 top: newAction.offsetTop + 10,
7396 left: 0,
7397 behavior: 'smooth'
7398 });
7399
7400 // Check if icon should be active
7401 checkActiveAction( type );
7402 showInputIcon( '#frm_form_action_' + actionId );
7403
7404 initiateMultiselect();
7405 initAutocomplete( newAction );
7406
7407 if ( widgetTop ) {
7408 jQuery( widgetTop ).trigger( 'frm-action-loaded' );
7409 }
7410
7411 /**
7412 * Fires after added a new form action.
7413 *
7414 * @since 5.5.4
7415 *
7416 * @param {HTMLElement} formAction Form action element.
7417 */
7418 frmAdminBuild.hooks.doAction( 'frm_added_form_action', newAction );
7419 }
7420 }
7421
7422 function closeOpenActions() {
7423 document.querySelectorAll( '.frm_form_action_settings.open' ).forEach(
7424 setting => setting.classList.remove( 'open' )
7425 );
7426 }
7427
7428 function toggleActionGroups() {
7429 /*jshint validthis:true */
7430 const actions = document.getElementById( 'frm_email_addon_menu' ).classList,
7431 search = document.getElementById( 'actions-search-input' );
7432
7433 if ( actions.contains( 'frm-all-actions' ) ) {
7434 actions.remove( 'frm-all-actions' );
7435 actions.add( 'frm-limited-actions' );
7436 } else {
7437 actions.add( 'frm-all-actions' );
7438 actions.remove( 'frm-limited-actions' );
7439 }
7440
7441 // Reset search.
7442 search.value = '';
7443 triggerEvent( search, 'input' );
7444 }
7445
7446 function getNewActionId() {
7447 let actionSettings = document.querySelectorAll( '.frm_form_action_settings' ),
7448 len = getNewRowId( actionSettings, 'frm_form_action_' );
7449 if ( typeof document.getElementById( 'frm_form_action_' + len ) !== 'undefined' ) {
7450 len = len + 100;
7451 }
7452 if ( lastNewActionIdReturned >= len ) {
7453 len = lastNewActionIdReturned + 1;
7454 }
7455 lastNewActionIdReturned = len;
7456 return len;
7457 }
7458
7459 function clickAction( obj ) {
7460 const $thisobj = jQuery( obj );
7461
7462 if ( obj.className.indexOf( 'selected' ) !== -1 ) {
7463 return;
7464 }
7465 if ( obj.className.indexOf( 'edit_field_type_end_divider' ) !== -1 && $thisobj.closest( '.edit_field_type_divider' ).hasClass( 'no_repeat_section' ) ) {
7466 return;
7467 }
7468
7469 deselectFields();
7470 $thisobj.addClass( 'selected' );
7471 showFieldOptions( obj );
7472 }
7473
7474 /**
7475 * When a field is selected, show the field settings in the sidebar.
7476 */
7477 function showFieldOptions( obj ) {
7478 let i, singleField,
7479 fieldId = obj.getAttribute( 'data-fid' ),
7480 fieldType = obj.getAttribute( 'data-type' ),
7481 allFieldSettings = document.querySelectorAll( '.frm-single-settings:not(.frm_hidden)' );
7482
7483 for ( i = 0; i < allFieldSettings.length; i++ ) {
7484 allFieldSettings[i].classList.add( 'frm_hidden' );
7485 }
7486
7487 singleField = document.getElementById( 'frm-single-settings-' + fieldId );
7488 moveFieldSettings( singleField );
7489
7490 if ( fieldType && 'quantity' === fieldType ) {
7491 popProductFields( jQuery( singleField ).find( '.frmjs_prod_field_opt' )[0]);
7492 }
7493
7494 singleField.classList.remove( 'frm_hidden' );
7495 document.getElementById( 'frm-options-panel-tab' ).click();
7496
7497 const editor = singleField.querySelector( '.wp-editor-area' );
7498 if ( editor ) {
7499 frmDom.wysiwyg.init(
7500 editor,
7501 { setupCallback: setupTinyMceEventHandlers }
7502 );
7503 }
7504
7505 wp.hooks.doAction( 'frmShowedFieldSettings', obj, singleField );
7506 maybeAddShortcodesModalTriggerIcon( fieldType, fieldId, singleField );
7507 }
7508
7509 function maybeAddShortcodesModalTriggerIcon( fieldType, fieldId, singleField ) {
7510 if ( ! shouldAddShortcodesModalTriggerIcon( fieldType ) ) {
7511 return;
7512 }
7513
7514 const fieldSettingsSelector = '#frm-single-settings-' + fieldId;
7515 if ( document.querySelector( fieldSettingsSelector + ' .frm-show-box' ) ) {
7516 return;
7517 }
7518 singleField.querySelector( '.wp-editor-container' )?.classList.add( 'frm_has_shortcodes' );
7519
7520 const wrapTextareaWithIconContainer = () => {
7521 const textareas = document.querySelectorAll( fieldSettingsSelector + ' .frm_has_shortcodes textarea' );
7522 textareas.forEach( textarea => {
7523 const wrapperSpan = span({ className: 'frm-with-right-icon' });
7524 textarea.parentNode.insertBefore( wrapperSpan, textarea );
7525 wrapperSpan.appendChild( createModalTriggerIcon() );
7526 wrapperSpan.appendChild( textarea );
7527 });
7528 };
7529
7530 const createModalTriggerIcon = () => {
7531 return frmDom.svg({ href: '#frm_more_horiz_solid_icon', classList: [ 'frm-show-box' ] });
7532 };
7533
7534 wrapTextareaWithIconContainer();
7535 }
7536
7537 function shouldAddShortcodesModalTriggerIcon( fieldType ) {
7538 const fieldsWithShortcodesBox = wp.hooks.applyFilters( 'frm_fields_with_shortcode_popup', [ 'html' ]);
7539
7540 return fieldsWithShortcodesBox.includes( fieldType );
7541 }
7542
7543 function setupTinyMceEventHandlers( editor ) {
7544 editor.on( 'Change', function() {
7545 handleTinyMceChange( editor );
7546 });
7547 }
7548
7549 function handleTinyMceChange( editor ) {
7550 if ( ! isTinyMceActive() || tinyMCE.activeEditor.isHidden() ) {
7551 return;
7552 }
7553
7554 editor.targetElm.value = editor.getContent();
7555 jQuery( editor.targetElm ).trigger( 'change' );
7556 }
7557
7558 function isTinyMceActive() {
7559 let activeSettings, wrapper;
7560
7561 activeSettings = document.querySelector( '.frm-single-settings:not(.frm_hidden)' );
7562 if ( ! activeSettings ) {
7563 return false;
7564 }
7565
7566 wrapper = activeSettings.querySelector( '.wp-editor-wrap' );
7567 return null !== wrapper && wrapper.classList.contains( 'tmce-active' );
7568 }
7569
7570 /**
7571 * Move the settings to the sidebar the first time they are changed or selected.
7572 * Keep the end marker at the end of the form.
7573 */
7574 function moveFieldSettings( singleField ) {
7575 let self = this;
7576
7577 if ( singleField === null ) {
7578 // The field may have not been loaded yet via ajax.
7579 return;
7580 }
7581
7582 this.fragment = document.createDocumentFragment();
7583
7584 this.initOnceInAllInstances = function() {
7585 if ( 'undefined' !== typeof moveFieldSettings.prototype.endMarker ) {
7586 return;
7587 }
7588 // perform a single search in the DOM and use it across all moveFieldSettings instances
7589 moveFieldSettings.prototype.endMarker = document.getElementById( 'frm-end-form-marker' );
7590 };
7591
7592 this.append = function( field ) {
7593 const classname = null !== field ? field.parentElement.classList : '';
7594 if ( null === field || ( ! classname.contains( 'frm_field_box' ) && ! classname.contains( 'divider_section_only' ) ) ) {
7595 return;
7596 }
7597 self.fragment.appendChild( field );
7598 };
7599
7600 this.moveFields = function() {
7601 builderForm.insertBefore( self.fragment, moveFieldSettings.prototype.endMarker );
7602 };
7603
7604 this.initOnceInAllInstances();
7605
7606 // Move the field if function is called as function with a singleField passed as arg.
7607 // In this particular case only 1 field is needed to be moved so the field will get instantly moved.
7608 // "singleField" may be undefined when it's called as a constructor instead of a function. Use the constructor to add multiple fields which are passed through "append" and move these all at once via "moveFields".
7609 if ( 'undefined' !== typeof singleField ) {
7610 this.append( singleField );
7611 this.moveFields();
7612 return;
7613 }
7614
7615 return {
7616 append: this.append,
7617 moveFields: this.moveFields
7618 };
7619
7620 }
7621
7622 function showEmailRow() {
7623 /*jshint validthis:true */
7624 const actionKey = jQuery( this ).closest( '.frm_form_action_settings' ).data( 'actionkey' );
7625 const rowType = this.getAttribute( 'data-emailrow' );
7626
7627 jQuery( '#frm_form_action_' + actionKey + ' .frm_' + rowType + '_row' ).fadeIn( 'slow' );
7628 jQuery( this ).fadeOut( 'slow' );
7629 }
7630
7631 function hideEmailRow() {
7632 /*jshint validthis:true */
7633 const actionBox = jQuery( this ).closest( '.frm_form_action_settings' ),
7634 rowType = this.getAttribute( 'data-emailrow' ),
7635 emailRowSelector = '.frm_' + rowType + '_row',
7636 emailButtonSelector = '.frm_' + rowType + '_button';
7637
7638 jQuery( actionBox ).find( emailButtonSelector ).fadeIn( 'slow' );
7639 jQuery( actionBox ).find( emailRowSelector ).fadeOut( 'slow', function() {
7640 jQuery( actionBox ).find( emailRowSelector + ' input' ).val( '' );
7641 });
7642 }
7643
7644 function showEmailWarning() {
7645 /*jshint validthis:true */
7646 const actionBox = jQuery( this ).closest( '.frm_form_action_settings' ),
7647 emailRowSelector = '.frm_from_to_match_row',
7648 fromVal = actionBox.find( 'input[name$="[post_content][from]"]' ).val(),
7649 toVal = actionBox.find( 'input[name$="[post_content][email_to]"]' ).val();
7650
7651 if ( fromVal === toVal ) {
7652 jQuery( actionBox ).find( emailRowSelector ).fadeIn( 'slow' );
7653 } else {
7654 jQuery( actionBox ).find( emailRowSelector ).fadeOut( 'slow' );
7655 }
7656 }
7657
7658 function checkActiveAction( type ) {
7659 const actionTriggers = document.querySelectorAll( '.frm_' + type + '_action' );
7660
7661 if ( isAtLimitForActionType( type ) ) {
7662 const addAlreadyUsedClass = getLimitForActionType( type ) > 0;
7663 markActionTriggersInactive( actionTriggers, addAlreadyUsedClass );
7664 return;
7665 }
7666
7667 markActionTriggersActive( actionTriggers );
7668 }
7669
7670 function markActionTriggersActive( triggers ) {
7671 triggers.forEach(
7672 trigger => {
7673 if ( trigger.querySelector( '.frm_show_upgrade' ) ) {
7674 // Prevent disabled action becoming active.
7675 return;
7676 }
7677
7678 trigger.classList.remove( 'frm_inactive_action', 'frm_already_used' );
7679 trigger.classList.add( 'frm_active_action' );
7680 }
7681 );
7682 }
7683
7684 function markActionTriggersInactive( triggers, addAlreadyUsedClass ) {
7685 triggers.forEach(
7686 trigger => {
7687 trigger.classList.remove( 'frm_active_action' );
7688 trigger.classList.add( 'frm_inactive_action' );
7689 if ( addAlreadyUsedClass ) {
7690 trigger.classList.add( 'frm_already_used' );
7691 }
7692 }
7693 );
7694 }
7695
7696 function isAtLimitForActionType( type ) {
7697 let atLimit = getNumberOfActionsForType( type ) >= getLimitForActionType( type );
7698
7699 const hookName = 'frm_action_at_limit';
7700 const hookArgs = { type };
7701 atLimit = wp.hooks.applyFilters( hookName, atLimit, hookArgs );
7702
7703 return atLimit;
7704 }
7705
7706 function getLimitForActionType( type ) {
7707 return parseInt( jQuery( '.frm_' + type + '_action' ).data( 'limit' ), 10 );
7708 }
7709
7710 function getNumberOfActionsForType( type ) {
7711 return jQuery( '.frm_single_' + type + '_settings' ).length;
7712 }
7713
7714 function actionLimitMessage() {
7715 let message = frmAdminJs.only_one_action;
7716 let limit = this.dataset.limit;
7717
7718 if ( 'undefined' !== typeof limit ) {
7719 limit = parseInt( limit );
7720 if ( limit > 1 ) {
7721 message = message.replace( 1, limit ).trim();
7722 } else {
7723 message += ' ' + frmAdminJs.edit_action_text;
7724 }
7725 }
7726
7727 infoModal( message );
7728 }
7729
7730 function addFormLogicRow() {
7731 /*jshint validthis:true */
7732 const id = jQuery( this ).data( 'emailkey' );
7733 const type = jQuery( this ).closest( '.frm_form_action_settings' ).find( '.frm_action_name' ).val();
7734 const formId = document.getElementById( 'form_id' ).value;
7735 const logicRowsContainer = document.getElementById( 'frm_logic_row_' + id );
7736 const logicRows = logicRowsContainer.querySelectorAll( '.frm_logic_row' );
7737 const newRowID = getNewRowId( logicRows, 'frm_logic_' + id + '_' );
7738 const placeholder = div({
7739 id: 'frm_logic_' + id + '_' + newRowID,
7740 className: 'frm_logic_row frm_hidden'
7741 });
7742
7743 logicRowsContainer.appendChild( placeholder );
7744 jQuery.ajax({
7745 type: 'POST', url: ajaxurl,
7746 data: {
7747 action: 'frm_add_form_logic_row',
7748 email_id: id,
7749 form_id: formId,
7750 meta_name: newRowID,
7751 type: type,
7752 nonce: frmGlobal.nonce
7753 },
7754 success: function( html ) {
7755 jQuery( document.getElementById( 'logic_link_' + id ) ).fadeOut( 'slow', () => {
7756 placeholder.insertAdjacentHTML( 'beforebegin', html );
7757 placeholder.remove();
7758
7759 // Show conditional logic options after "Add Conditional Logic" is clicked.
7760 jQuery( logicRowsContainer ).parent( '.frm_logic_rows' ).fadeIn( 'slow' );
7761 });
7762 }
7763 });
7764 return false;
7765 }
7766
7767 function checkDupPost() {
7768 /*jshint validthis:true */
7769 const postField = jQuery( 'select.frm_single_post_field' );
7770 postField.css( 'border-color', '' );
7771 const $t = this;
7772 const v = jQuery( $t ).val();
7773 if ( v === '' || v === 'checkbox' ) {
7774 return false;
7775 }
7776 postField.each( function() {
7777 if ( jQuery( this ).val() === v && this.name !== $t.name ) {
7778 this.style.borderColor = 'red';
7779 jQuery( $t ).val( '' );
7780 infoModal( frmAdminJs.field_already_used );
7781 return false;
7782 }
7783 });
7784 }
7785
7786 function togglePostContent() {
7787 /*jshint validthis:true */
7788 const v = jQuery( this ).val();
7789 if ( '' === v ) {
7790 jQuery( '.frm_post_content_opt, select.frm_dyncontent_opt' ).hide().val( '' );
7791 jQuery( '.frm_dyncontent_opt' ).hide();
7792 } else if ( 'post_content' === v ) {
7793 jQuery( '.frm_post_content_opt' ).show();
7794 jQuery( '.frm_dyncontent_opt' ).hide();
7795 jQuery( 'select.frm_dyncontent_opt' ).val( '' );
7796 } else {
7797 jQuery( '.frm_post_content_opt' ).hide().val( '' );
7798 jQuery( 'select.frm_dyncontent_opt, .frm_form_field.frm_dyncontent_opt' ).show();
7799 }
7800 }
7801
7802 function fillDyncontent() {
7803 /*jshint validthis:true */
7804 const v = jQuery( this ).val();
7805 const $dyn = jQuery( document.getElementById( 'frm_dyncontent' ) );
7806 if ( '' === v || 'new' === v ) {
7807 $dyn.val( '' );
7808 jQuery( '.frm_dyncontent_opt' ).show();
7809 } else {
7810 jQuery.ajax({
7811 type: 'POST', url: ajaxurl,
7812 data: {action: 'frm_display_get_content', id: v, nonce: frmGlobal.nonce},
7813 success: function( val ) {
7814 $dyn.val( val );
7815 jQuery( '.frm_dyncontent_opt' ).show();
7816 }
7817 });
7818 }
7819 }
7820
7821 function switchPostType() {
7822 /*jshint validthis:true */
7823 // update all rows of categories/taxonomies
7824 let curSelect, newSelect,
7825 catRows = document.getElementById( 'frm_posttax_rows' ).childNodes,
7826 postParentField = document.querySelector( '.frm_post_parent_field' ),
7827 postMenuOrderField = document.querySelector( '.frm_post_menu_order_field' ),
7828 postType = this.value;
7829
7830 // Get new category/taxonomy options
7831 jQuery.ajax({
7832 type: 'POST',
7833 url: ajaxurl,
7834 data: {
7835 action: 'frm_replace_posttax_options',
7836 post_type: postType,
7837 nonce: frmGlobal.nonce
7838 },
7839 success: function( html ) {
7840
7841 // Loop through each category row, and replace the first dropdown
7842 for ( i = 0; i < catRows.length; i++ ) {
7843 // Check if current element is a div
7844 if ( catRows[i].tagName !== 'DIV' ) {
7845 continue;
7846 }
7847
7848 // Get current category select
7849 curSelect = catRows[i].getElementsByTagName( 'select' )[0];
7850
7851 // Set up new select
7852 newSelect = document.createElement( 'select' );
7853 newSelect.innerHTML = html;
7854 newSelect.className = curSelect.className;
7855 newSelect.name = curSelect.name;
7856
7857 // Replace the old select with the new select
7858 catRows[i].replaceChild( newSelect, curSelect );
7859 }
7860 }
7861 });
7862
7863 // Get new post parent option.
7864 if ( postParentField ) {
7865 getActionOption(
7866 postParentField,
7867 postType,
7868 'frm_get_post_parent_option',
7869 function( response, optName ) {
7870 // The replaced string is declared in FrmProFormActionController::ajax_get_post_menu_order_option() in the pro version.
7871 postParentField.querySelector( '.frm_post_parent_opt_wrapper' ).innerHTML = response.replaceAll( 'REPLACETHISNAME', optName );
7872 initAutocomplete( postParentField );
7873 }
7874 );
7875 }
7876
7877 if ( postMenuOrderField ) {
7878 getActionOption( postMenuOrderField, postType, 'frm_should_use_post_menu_order_option' );
7879 }
7880 }
7881
7882 function getActionOption( field, postType, action, successHandler ) {
7883 const opt = field.querySelector( '.frm_autocomplete_value_input' ) || field.querySelector( 'select' ),
7884 optName = opt.getAttribute( 'name' );
7885
7886 jQuery.ajax({
7887 url: ajaxurl,
7888 method: 'POST',
7889 data: {
7890 action: action,
7891 post_type: postType,
7892 _wpnonce: frmGlobal.nonce
7893 },
7894 success: response => {
7895 if ( 'string' !== typeof response ) {
7896 console.error( response );
7897 return;
7898 }
7899
7900 if ( '0' === response ) {
7901 // This post type does not support this field.
7902 field.classList.add( 'frm_hidden' );
7903 field.value = '';
7904 return;
7905 }
7906
7907 field.classList.remove( 'frm_hidden' );
7908
7909 if ( 'function' === typeof successHandler ) {
7910 successHandler( response, optName );
7911 }
7912 },
7913 error: response => console.error( response )
7914 });
7915 }
7916
7917 function addPosttaxRow() {
7918 /*jshint validthis:true */
7919 addPostRow( 'tax', this );
7920 }
7921
7922 function addPostmetaRow() {
7923 /*jshint validthis:true */
7924 addPostRow( 'meta', this );
7925 }
7926
7927 function addPostRow( type, button ) {
7928 let name,
7929 id = jQuery( 'input[name="id"]' ).val(),
7930 settings = jQuery( button ).closest( '.frm_form_action_settings' ),
7931 key = settings.data( 'actionkey' ),
7932 postType = settings.find( '.frm_post_type' ).val(),
7933 metaName = 0,
7934 postTypeRows = document.querySelectorAll( '.frm_post' + type + '_row' );
7935
7936 if ( postTypeRows.length ) {
7937 name = postTypeRows[ postTypeRows.length - 1 ].id.replace( 'frm_post' + type + '_', '' );
7938 if ( isNumeric( name ) ) {
7939 metaName = 1 + parseInt( name, 10 );
7940 } else {
7941 metaName = 1;
7942 }
7943 }
7944
7945 jQuery.ajax({
7946 type: 'POST', url: ajaxurl,
7947 data: {
7948 action: 'frm_add_post' + type + '_row',
7949 form_id: id,
7950 meta_name: metaName,
7951 tax_key: metaName,
7952 post_type: postType,
7953 action_key: key,
7954 nonce: frmGlobal.nonce
7955 },
7956 success: function( html ) {
7957 let cfOpts, optIndex;
7958 jQuery( document.getElementById( 'frm_post' + type + '_rows' ) ).append( html );
7959 jQuery( '.frm_add_post' + type + '_row.button' ).hide();
7960
7961 if ( type === 'meta' ) {
7962 jQuery( '.frm_name_value' ).show();
7963 cfOpts = document.querySelectorAll( '.frm_toggle_cf_opts' );
7964 for ( optIndex = 0; optIndex < cfOpts.length - 1; ++optIndex ) {
7965 cfOpts[ optIndex ].style.display = 'none';
7966 }
7967 } else if ( type === 'tax' ) {
7968 jQuery( '.frm_posttax_labels' ).show();
7969 }
7970 }
7971 });
7972 }
7973
7974 function isNumeric( value ) {
7975 return ! isNaN( parseFloat( value ) ) && isFinite( value );
7976 }
7977
7978 function changePosttaxRow() {
7979 /*jshint validthis:true */
7980 if ( ! jQuery( this ).closest( '.frm_posttax_row' ).find( '.frm_posttax_opt_list' ).length ) {
7981 return;
7982 }
7983
7984 jQuery( this ).closest( '.frm_posttax_row' ).find( '.frm_posttax_opt_list' ).html( '<div class="spinner frm_spinner" style="display:block"></div>' );
7985
7986 const postType = jQuery( this ).closest( '.frm_form_action_settings' ).find( 'select[name$="[post_content][post_type]"]' ).val(),
7987 actionKey = jQuery( this ).closest( '.frm_form_action_settings' ).data( 'actionkey' ),
7988 taxKey = jQuery( this ).closest( '.frm_posttax_row' ).attr( 'id' ).replace( 'frm_posttax_', '' ),
7989 metaName = jQuery( this ).val(),
7990 showExclude = jQuery( document.getElementById( taxKey + '_show_exclude' ) ).is( ':checked' ) ? 1 : 0,
7991 fieldId = jQuery( 'select[name$="[post_category][' + taxKey + '][field_id]"]' ).val(),
7992 id = jQuery( 'input[name="id"]' ).val();
7993
7994 jQuery.ajax({
7995 type: 'POST',
7996 url: ajaxurl,
7997 data: {
7998 action: 'frm_add_posttax_row',
7999 form_id: id,
8000 post_type: postType,
8001 tax_key: taxKey,
8002 action_key: actionKey,
8003 meta_name: metaName,
8004 field_id: fieldId,
8005 show_exclude: showExclude,
8006 nonce: frmGlobal.nonce
8007 },
8008 success: function( html ) {
8009 const $tax = jQuery( document.getElementById( 'frm_posttax_' + taxKey ) );
8010 $tax.replaceWith( html );
8011 }
8012 });
8013 }
8014
8015 function toggleCfOpts() {
8016 /*jshint validthis:true */
8017 const row = jQuery( this ).closest( '.frm_postmeta_row' );
8018 const cancel = row.find( '.frm_cancelnew' );
8019 const select = row.find( '.frm_enternew' );
8020 if ( row.find( 'select.frm_cancelnew' ).is( ':visible' ) ) {
8021 cancel.hide();
8022 select.show();
8023 } else {
8024 cancel.show();
8025 select.hide();
8026 }
8027
8028 row.find( 'input.frm_enternew, select.frm_cancelnew' ).val( '' );
8029 return false;
8030 }
8031
8032 function toggleFormOpts() {
8033 /*jshint validthis:true */
8034 const changedOpt = jQuery( this );
8035 let val = changedOpt.val();
8036 if ( changedOpt.attr( 'type' ) === 'checkbox' ) {
8037 if ( this.checked === false ) {
8038 val = '';
8039 }
8040 }
8041
8042 const toggleClass = changedOpt.data( 'toggleclass' );
8043 if ( val === '' ) {
8044 jQuery( '.' + toggleClass ).hide();
8045 } else {
8046 jQuery( '.' + toggleClass ).show();
8047 jQuery( '.hide_' + toggleClass + '_' + val ).hide();
8048 }
8049 }
8050
8051 function submitSettings() {
8052 if ( showNameYourFormModal() ) {
8053 return;
8054 }
8055
8056 /*jshint validthis:true */
8057 preFormSave( this );
8058 triggerSubmit( document.querySelector( '.frm_form_settings' ) );
8059 }
8060
8061 /* Customization Panel */
8062 function insertCode( e ) {
8063 /*jshint validthis:true */
8064 e.preventDefault();
8065 insertFieldCode( jQuery( this ), this.getAttribute( 'data-code' ) );
8066 return false;
8067 }
8068
8069 function insertFieldCode( element, variable ) {
8070 let rich = false,
8071 elementId = element;
8072 if ( typeof element === 'object' ) {
8073 if ( element.hasClass( 'frm_noallow' ) ) {
8074 return;
8075 }
8076
8077 elementId = jQuery( element ).closest( '[data-fills]' ).attr( 'data-fills' );
8078 if ( typeof elementId === 'undefined' ) {
8079 elementId = element.closest( 'div' ).attr( 'class' );
8080 if ( typeof elementId !== 'undefined' ) {
8081 elementId = elementId.split( ' ' )[1];
8082 }
8083 }
8084 }
8085
8086 if ( typeof elementId === 'undefined' ) {
8087 let active = document.activeElement;
8088 if ( active.type === 'search' ) {
8089 // If the search field has focus, find the correct field.
8090 elementId = active.id.replace( '-search-input', '' );
8091 if ( elementId.match( /\d/gi ) === null ) {
8092 active = jQuery( '.frm-single-settings:visible .' + elementId );
8093 elementId = active.attr( 'id' );
8094 }
8095 } else {
8096 elementId = active.id;
8097 }
8098 }
8099
8100 if ( elementId ) {
8101 rich = jQuery( '#wp-' + elementId + '-wrap.wp-editor-wrap' ).length > 0;
8102 }
8103
8104 const contentBox = jQuery( document.getElementById( elementId ) );
8105 if ( typeof element.attr( 'data-shortcode' ) === 'undefined' && ( ! contentBox.length || typeof contentBox.attr( 'data-shortcode' ) === 'undefined' ) ) {
8106 // this helps to exclude those that don't want shortcode-like inserted content e.g. frm-pro's summary field
8107 const doShortcode = element.parents( 'ul.frm_code_list' ).attr( 'data-shortcode' );
8108 if ( doShortcode === 'undefined' || doShortcode !== 'no' ) {
8109 variable = '[' + variable + ']';
8110 }
8111 }
8112
8113 if ( rich ) {
8114 wpActiveEditor = elementId;
8115 }
8116
8117 if ( ! contentBox.length ) {
8118 return false;
8119 }
8120
8121 if ( variable === '[default-html]' || variable === '[default-plain]' ) {
8122 let p = 0;
8123 if ( variable === '[default-plain]' ) {
8124 p = 1;
8125 }
8126 jQuery.ajax({
8127 type: 'POST', url: ajaxurl,
8128 data: {
8129 action: 'frm_get_default_html',
8130 form_id: jQuery( 'input[name="id"]' ).val(),
8131 plain_text: p,
8132 nonce: frmGlobal.nonce
8133 },
8134 elementId: elementId,
8135 success: function( msg ) {
8136 if ( rich ) {
8137 const p = document.createElement( 'p' );
8138 p.innerText = msg;
8139 send_to_editor( p.innerHTML );
8140 } else {
8141 insertContent( contentBox, msg );
8142 }
8143 }
8144 });
8145 } else {
8146 variable = maybeAddSanitizeUrlToShortcodeVariable( variable, element, contentBox );
8147 if ( rich ) {
8148 send_to_editor( variable );
8149 } else {
8150 insertContent( contentBox, variable );
8151 }
8152 }
8153 return false;
8154 }
8155
8156 function maybeAddSanitizeUrlToShortcodeVariable( variable, element, contentBox ) {
8157 if ( 'object' !== typeof element || ! ( element instanceof jQuery ) || 0 !== contentBox[0].id.indexOf( 'success_url_' ) ) {
8158 return variable;
8159 }
8160
8161 element = element[0];
8162 if ( ! element.closest( '#frm-insert-fields-box' ) ) {
8163 // Only add sanitize_url=1 to field shortcodes.
8164 return variable;
8165 }
8166
8167 if ( ! element.parentNode.classList.contains( 'frm_insert_url' ) ) {
8168 variable = variable.replace( ']', ' sanitize_url=1]' );
8169 }
8170
8171 return variable;
8172 }
8173
8174 function insertContent( contentBox, variable ) {
8175 if ( document.selection ) {
8176 contentBox[0].focus();
8177 document.selection.createRange().text = variable;
8178 } else {
8179 obj = contentBox[0];
8180 const e = obj.selectionEnd;
8181
8182 variable = maybeFormatInsertedContent( contentBox, variable, obj.selectionStart, e );
8183
8184 obj.value = obj.value.substr( 0, obj.selectionStart ) + variable + obj.value.substr( obj.selectionEnd, obj.value.length );
8185
8186 const s = e + variable.length;
8187
8188 maybeRemoveLayoutClasses( obj, variable );
8189
8190 obj.focus();
8191 obj.setSelectionRange( s, s );
8192 }
8193 triggerChange( contentBox );
8194 }
8195
8196 /**
8197 * When a layout class is added, remove any previous layout classes to avoid conflicts.
8198 * We only expect one layout class to exist for a given field.
8199 * For example, if a field has frm_half and we set it to frm_third, frm_half will be removed.
8200 *
8201 * @since 6.11
8202 *
8203 * @param {HTMLElement} obj
8204 * @param {string} variable
8205 * @return {void}
8206 */
8207 function maybeRemoveLayoutClasses( obj, variable ) {
8208 if ( ! obj.classList.contains( 'frm_classes' ) || ! isALayoutClass( variable ) ) {
8209 return;
8210 }
8211
8212 const removeClasses = obj.value.split( ' ' ).filter( isALayoutClass );
8213 if ( removeClasses.length ) {
8214 obj.value = maybeRemoveClasses( obj.value, removeClasses, variable.trim() );
8215 }
8216 }
8217
8218 /**
8219 * Check if a given class is a layout class.
8220 *
8221 * @since 6.11
8222 *
8223 * @param {string} className
8224 * @return {boolean}
8225 */
8226 function isALayoutClass( className ) {
8227 let layoutClasses = [ 'frm_half', 'frm_third', 'frm_two_thirds', 'frm_fourth', 'frm_three_fourths', 'frm_fifth', 'frm_sixth', 'frm2', 'frm3', 'frm4', 'frm6', 'frm8', 'frm9', 'frm10', 'frm12' ];
8228 return layoutClasses.includes( className.trim() );
8229 }
8230
8231 /**
8232 * @since 6.11
8233 *
8234 * @param {string} beforeValue
8235 * @param {Array} removeClasses
8236 * @param {string} variable
8237 * @return {string}
8238 */
8239 function maybeRemoveClasses( beforeValue, removeClasses, variable ) {
8240 const currentClasses = beforeValue.split( ' ' ).filter(
8241 currentClass => {
8242 currentClass = currentClass.trim();
8243 return currentClass.length && ! removeClasses.includes( currentClass );
8244 }
8245 );
8246 if ( ! currentClasses.includes( variable ) ) {
8247 currentClasses.push( variable );
8248 }
8249 return currentClasses.join( ' ' );
8250 }
8251
8252 function maybeFormatInsertedContent( input, textToInsert, selectionStart, selectionEnd ) {
8253 const separator = input.data( 'sep' );
8254 if ( undefined === separator ) {
8255 return textToInsert;
8256 }
8257
8258 const value = input.val();
8259
8260 if ( ! value.trim().length ) {
8261 return textToInsert;
8262 }
8263
8264 const startPattern = new RegExp( separator + '\\s*$' );
8265 const endPattern = new RegExp( '^\\s*' + separator );
8266
8267 if ( value.substr( 0, selectionStart ).trim().length && false === startPattern.test( value.substr( 0, selectionStart ) ) ) {
8268 textToInsert = separator + textToInsert;
8269 }
8270
8271 if ( value.substr( selectionEnd, value.length ).trim().length && false === endPattern.test( value.substr( selectionEnd, value.length ) ) ) {
8272 textToInsert += separator;
8273 }
8274
8275 return textToInsert;
8276 }
8277
8278 function resetLogicBuilder() {
8279 /*jshint validthis:true */
8280 const id = document.getElementById( 'frm-id-condition' ),
8281 key = document.getElementById( 'frm-key-condition' );
8282
8283 if ( this.checked ) {
8284 id.classList.remove( 'frm_hidden' );
8285 key.classList.add( 'frm_hidden' );
8286 triggerEvent( key, 'change' );
8287 } else {
8288 id.classList.add( 'frm_hidden' );
8289 key.classList.remove( 'frm_hidden' );
8290 triggerEvent( id, 'change' );
8291 }
8292 }
8293
8294 function setLogicExample() {
8295 let field, code,
8296 idKey = document.getElementById( 'frm-id-key-condition' ).checked ? 'frm-id-condition' : 'frm-key-condition',
8297 is = document.getElementById( 'frm-is-condition' ).value,
8298 text = document.getElementById( 'frm-text-condition' ).value,
8299 result = document.getElementById( 'frm-insert-condition' );
8300
8301 idKey = document.getElementById( idKey );
8302 field = idKey.options[idKey.selectedIndex].value;
8303 code = 'if ' + field + ' ' + is + '="' + text + '"]';
8304 result.setAttribute( 'data-code', code + frmAdminJs.conditional_text + '[/if ' + field );
8305 result.innerHTML = '[' + code + '[/if ' + field + ']';
8306 }
8307
8308 function showBuilderModal() {
8309 /*jshint validthis:true */
8310 const moreIcon = getIconForInput( this );
8311 showInlineModal( moreIcon, this );
8312 }
8313
8314 function maybeShowModal( input ) {
8315 let moreIcon;
8316 if ( input.parentNode.parentNode.classList.contains( 'frm_has_shortcodes' ) ) {
8317 hideShortcodes();
8318 moreIcon = getIconForInput( input );
8319 if ( moreIcon.tagName === 'use' ) {
8320 moreIcon = moreIcon.firstElementChild;
8321
8322 if ( moreIcon.getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' ).indexOf( 'frm_close_icon' ) === -1 ) {
8323 showShortcodeBox( moreIcon, 'nofocus' );
8324 }
8325 } else if ( ! moreIcon.classList.contains( 'frm_close_icon' ) ) {
8326 showShortcodeBox( moreIcon, 'nofocus' );
8327 }
8328 }
8329 }
8330
8331 function showShortcodes( e ) {
8332 /*jshint validthis:true */
8333 e.preventDefault();
8334 e.stopPropagation();
8335
8336 showShortcodeBox( this );
8337 }
8338
8339 /**
8340 * Handles 'change' event on the document.
8341 *
8342 * @since 6.16.3
8343 *
8344 * @param {Event} event
8345 * @returns {Void}
8346 */
8347 function handleBuilderChangeEvent( event ) {
8348 maybeShowSaveAndReloadModal( event.target );
8349 }
8350
8351 /**
8352 * Shows 'Save and Reload' modal if the target field's type is changed.
8353 *
8354 * @since 6.16.3
8355 *
8356 * @param {HTMLElement} target
8357 * @returns {Void}
8358 */
8359 function maybeShowSaveAndReloadModal( target ) {
8360 if ( ! target.id.startsWith( 'field_options_type_' ) ) {
8361 return;
8362 }
8363 const idParts = target.id.split( '_' );
8364 const fieldId = idParts.length && idParts[ idParts.length - 1 ];
8365
8366 if ( document.querySelector( `#frm-single-settings-${fieldId}` )?.classList.contains( `frm-type-${target.value}` ) ) {
8367 // Do not show modal if the field type is reverted back to the original type when builder is loaded.
8368 return;
8369 }
8370 showSaveAndReloadModal();
8371 }
8372
8373 /**
8374 * Shows 'Save and Reload' modal with the given message.
8375 *
8376 * @since 6.16.3
8377 *
8378 * @param {string} message
8379 * @returns {Void}
8380 */
8381 function showSaveAndReloadModal( message ) {
8382 if ( 'undefined' === typeof message ) {
8383 message = __( 'You are changing the field type. Not all field settings will appear as expected until you reload the page. Would you like to reload the page now?', 'formidable' );
8384 }
8385 frmDom.modal.maybeCreateModal(
8386 'frmSaveAndReloadModal',
8387 {
8388 title: __( 'Save and Reload?', 'formidable' ),
8389 content: getModalContent(),
8390 footer: getModalFooter()
8391 }
8392 );
8393
8394 function getModalContent() {
8395 const modalContent = div( message );
8396 modalContent.style.padding = 'var(--gap-md)';
8397 return modalContent;
8398 }
8399
8400 function getModalFooter() {
8401 const continueButton = frmDom.modal.footerButton({
8402 text: __( 'Save and Reload', 'formidable' ),
8403 buttonType: 'primary'
8404 });
8405
8406 onClickPreventDefault( continueButton, () => {
8407 saveAndReloadFormBuilder();
8408 } );
8409
8410 const cancelButton = frmDom.modal.footerButton({
8411 text: __( 'Cancel', 'formidable' ),
8412 buttonType: 'cancel'
8413 });
8414 cancelButton.classList.add( 'dismiss' );
8415
8416 return frmDom.div({
8417 children: [ cancelButton, continueButton ]
8418 });
8419 }
8420 }
8421
8422 function updateShortcodesPopupPosition( target ) {
8423 let moreIcon;
8424 if ( target instanceof Event ) {
8425 const useElements = document.querySelectorAll( '.frm-single-settings .frm-show-box.frmsvg use' );
8426 const openTrigger = Array.from( useElements ).find( use => use.getAttribute( 'href' ) === '#frm_close_icon' );
8427 if ( 'undefined' === typeof openTrigger ) {
8428 return;
8429 }
8430 moreIcon = openTrigger.parentElement;
8431 } else {
8432 moreIcon = target;
8433 }
8434
8435 const moreIconPosition = moreIcon.getBoundingClientRect();
8436 const shortCodesPopup = document.getElementById( 'frm_adv_info' );
8437 const parentPos = shortCodesPopup.parentElement.getBoundingClientRect();
8438
8439 shortCodesPopup.style.top = ( moreIconPosition.top - parentPos.top + 32 ) + 'px';
8440 shortCodesPopup.style.left = ( moreIconPosition.left - parentPos.left - 280 ) + 'px';
8441 }
8442
8443 function showShortcodeBox( moreIcon, shouldFocus ) {
8444 let input = getInputForIcon( moreIcon ),
8445 box = document.getElementById( 'frm_adv_info' ),
8446 classes = moreIcon.className;
8447
8448 if ( moreIcon.tagName === 'svg' ) {
8449 moreIcon = moreIcon.firstElementChild;
8450 }
8451 if ( moreIcon.tagName === 'use' ) {
8452 classes = moreIcon.getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
8453
8454 if ( null === classes ) {
8455 // If the deprecated xlink:href is not defined, check for href.
8456 classes = moreIcon.getAttribute( 'href' );
8457 }
8458 }
8459
8460 if ( classes.indexOf( 'frm_close_icon' ) !== -1 ) {
8461 hideShortcodes( box );
8462 } else {
8463 updateShortcodesPopupPosition( moreIcon );
8464
8465 jQuery( '.frm_code_list a' ).removeClass( 'frm_noallow' );
8466 if ( input.classList.contains( 'frm_not_email_to' ) ) {
8467 jQuery( '#frm-insert-fields-box .frm_code_list li:not(.show_frm_not_email_to) a' ).addClass( 'frm_noallow' );
8468 } else if ( input.classList.contains( 'frm_not_email_subject' ) ) {
8469 jQuery( '.frm_code_list li.hide_frm_not_email_subject a' ).addClass( 'frm_noallow' );
8470 }
8471
8472 box.setAttribute( 'data-fills', input.id );
8473 box.style.display = 'block';
8474
8475 if ( moreIcon.tagName === 'use' ) {
8476 if ( moreIcon.hasAttributeNS( 'http://www.w3.org/1999/xlink', 'href' ) ) {
8477 moreIcon.setAttributeNS( 'http://www.w3.org/1999/xlink', 'href', '#frm_close_icon' );
8478 } else {
8479 const newMoreIcon = document.createElementNS( 'http://www.w3.org/2000/svg', 'use' );
8480 newMoreIcon.setAttributeNS( 'http://www.w3.org/1999/xlink', 'href', '#frm_close_icon' );
8481 moreIcon.parentNode.replaceChild( newMoreIcon, moreIcon );
8482 }
8483 } else {
8484 moreIcon.className = classes.replace( 'frm_more_horiz_solid_icon', 'frm_close_icon' );
8485 }
8486
8487 if ( shouldFocus !== 'nofocus' ) {
8488 if ( 'none' !== input.style.display ) {
8489 input.focus();
8490 } else {
8491 jQuery( tinymce.get( input.id ) ).trigger( 'focus' );
8492 }
8493 }
8494 showOrHideContextualShortcodes( input );
8495 }
8496 }
8497
8498 /**
8499 * Returns true if a shortcode could be shown in the search result.
8500 *
8501 * @since 6.16.3
8502 *
8503 * @param {HTMLElement} item
8504 * @returns {Boolean}
8505 */
8506 function checkContextualShortcode( item ) {
8507 if ( frmAdminJs.contextualShortcodes.length === 0 ) {
8508 return true;
8509 }
8510 return ! isContextualShortcode( item ) || canShowContextualShortcode( item );
8511 }
8512
8513 /**
8514 * Returns true if a shortcode is contextual to fields.
8515 *
8516 * @since 6.16.3
8517 *
8518 * @param {HTMLElement} item
8519 * @returns {Boolean}
8520 */
8521 function isContextualShortcode( item ) {
8522 const anchor = item.querySelector( 'a' );
8523 if ( ! anchor ) {
8524 return false;
8525 }
8526
8527 const shortcode = anchor.dataset.code;
8528 return frmAdminJs.contextualShortcodes.address.includes( shortcode ) || frmAdminJs.contextualShortcodes.body.includes( shortcode );
8529 }
8530
8531 /**
8532 * @since 6.16.3
8533 *
8534 * @param {HTMLElement} item
8535 * @returns {Boolean}
8536 */
8537 function canShowContextualShortcode( item ) {
8538 const shortcode = item.querySelector( 'a' ).dataset.code;
8539 const inputId = document.getElementById( 'frm_adv_info' ).dataset.fills;
8540 const input = document.getElementById( inputId );
8541 const contextualShortcodes = frmAdminJs.contextualShortcodes;
8542 if ( contextualShortcodes.address.includes( shortcode ) ) {
8543 return input.matches( contextualShortcodes.addressSelector );
8544 }
8545 return input.matches( contextualShortcodes.bodySelector );
8546 }
8547
8548 /**
8549 * @since 6.16.3
8550 *
8551 * @param {HTMLElement} input
8552 * @returns {Void}
8553 */
8554 function showOrHideContextualShortcodes( input ) {
8555 [ 'address', 'body' ].forEach( type => {
8556 toggleContextualShortcodes( input, type );
8557 });
8558 }
8559
8560 /**
8561 * @since 6.16.3
8562 *
8563 * @param {HTMLElement} input
8564 * @param {string} type
8565 *
8566 * @returns {Void}
8567 */
8568 function toggleContextualShortcodes( input, type ) {
8569 let selector, contextualShortcodes;
8570 selector = frmAdminJs.contextualShortcodes[ type + 'Selector' ];
8571 contextualShortcodes = frmAdminJs.contextualShortcodes[ type ];
8572 let shouldShowShortcodes = input.matches( selector );
8573 for ( let shortcode of contextualShortcodes ) {
8574 const shortcodeLi = document.querySelector( '#frm-adv-info-tab .frm_code_list [data-code="' + shortcode + '"]' )?.closest( 'li');
8575 shortcodeLi?.classList.toggle( 'frm_hidden', ! shouldShowShortcodes );
8576 }
8577 }
8578
8579 /**
8580 * Returns shortcodes that are contextual to the current input field.
8581 *
8582 * @since 6.16.3
8583 *
8584 * @returns {Array}
8585 */
8586 function getContextualShortcodes() {
8587 let contextualShortcodes = document.getElementById( 'frm_adv_info' )?.dataset.contextualShortcodes;
8588 if ( ! contextualShortcodes) {
8589 return [];
8590 }
8591 contextualShortcodes = JSON.parse( contextualShortcodes );
8592 contextualShortcodes.addressSelector = '[id^=email_to], [id^=from_], [id^=cc], [id^=bcc]';
8593 contextualShortcodes.bodySelector = '[id^=email_message_]';
8594 return contextualShortcodes;
8595 }
8596
8597 function fieldUpdated() {
8598 if ( ! fieldsUpdated ) {
8599 fieldsUpdated = 1;
8600 window.addEventListener( 'beforeunload', confirmExit );
8601 }
8602 }
8603
8604 function buildSubmittedNoAjax() {
8605 // set fieldsUpdated to 0 to avoid the unsaved changes pop up
8606 fieldsUpdated = 0;
8607 }
8608
8609 function settingsSubmitted() {
8610 // set fieldsUpdated to 0 to avoid the unsaved changes pop up
8611 fieldsUpdated = 0;
8612 }
8613
8614 function saveAndReloadSettings() {
8615 let page, form;
8616 page = document.getElementById( 'form_settings_page' );
8617 if ( null !== page ) {
8618 form = page.querySelector( 'form.frm_form_settings' );
8619 if ( null !== form ) {
8620 fieldsUpdated = 0;
8621 form.submit();
8622 }
8623 }
8624 }
8625
8626 function reloadIfAddonActivatedAjaxSubmitOnly() {
8627 const submitButton = document.getElementById( 'frm_submit_side_top' );
8628 if ( submitButton.hasAttribute( 'data-new-addon-installed' ) && 'true' === submitButton.getAttribute( 'data-new-addon-installed' ) ) {
8629 submitButton.removeAttribute( 'data-new-addon-installed' );
8630 window.location.reload();
8631 }
8632
8633 }
8634
8635 function saveAndReloadFormBuilder() {
8636 const submitButton = document.getElementById( 'frm_submit_side_top' );
8637 if ( submitButton.classList.contains( 'frm_submit_ajax' ) ) {
8638 submitButton.setAttribute( 'data-new-addon-installed', true );
8639 }
8640 submitButton.click();
8641 }
8642
8643 function confirmExit( event ) {
8644 if ( fieldsUpdated ) {
8645 event.preventDefault();
8646 event.returnValue = '';
8647 }
8648 }
8649
8650 function bindClickForDialogClose( $modal ) {
8651 const closeModal = function() {
8652 $modal.dialog( 'close' );
8653 };
8654 jQuery( '.ui-widget-overlay' ).on( 'click', closeModal );
8655 $modal.on( 'click', 'a.dismiss', closeModal );
8656 }
8657
8658 function offsetModalY( $modal, amount ) {
8659 const position = {
8660 my: 'top',
8661 at: 'top+' + amount,
8662 of: window
8663 };
8664 $modal.dialog( 'option', 'position', position );
8665 }
8666
8667 /**
8668 * Get the input box for the selected ... icon.
8669 */
8670 function getInputForIcon( moreIcon ) {
8671 let input = moreIcon.nextElementSibling;
8672
8673 while ( input !== null && input.tagName !== 'INPUT' && input.tagName !== 'TEXTAREA' ) {
8674 input = getInputForIcon( input );
8675 }
8676
8677 return input;
8678 }
8679
8680 /**
8681 * Get the ... icon for the selected input box.
8682 */
8683 function getIconForInput( input ) {
8684 let moreIcon = input.previousElementSibling;
8685
8686 while ( moreIcon !== null && moreIcon.tagName !== 'I' && moreIcon.tagName !== 'svg' ) {
8687 moreIcon = getIconForInput( moreIcon );
8688 }
8689
8690 return moreIcon;
8691 }
8692
8693 function hideShortcodes( box ) {
8694 let i, u, closeIcons, closeSvg;
8695 if ( typeof box === 'undefined' ) {
8696 box = document.getElementById( 'frm_adv_info' );
8697 if ( box === null ) {
8698 return;
8699 }
8700 }
8701
8702 if ( document.getElementById( 'frm_dyncontent' ) !== null ) {
8703 // Don't run when in the sidebar.
8704 return;
8705 }
8706
8707 box.style.display = 'none';
8708
8709 closeIcons = document.querySelectorAll( '.frm-show-box.frm_close_icon' );
8710 for ( i = 0; i < closeIcons.length; i++ ) {
8711 closeIcons[i].classList.remove( 'frm_close_icon' );
8712 closeIcons[i].classList.add( 'frm_more_horiz_solid_icon' );
8713 }
8714
8715 closeSvg = document.querySelectorAll( '.frm_has_shortcodes use' );
8716 for ( u = 0; u < closeSvg.length; u++ ) {
8717 if ( closeSvg[u].getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' ) === '#frm_close_icon' ) {
8718 if ( closeSvg[u].closest( '.frm_remove_field' ) ) {
8719 // Don't change the icon for the email fields remove button.
8720 continue;
8721 }
8722 closeSvg[u].setAttributeNS( 'http://www.w3.org/1999/xlink', 'href', '#frm_more_horiz_solid_icon' );
8723 }
8724 }
8725 }
8726
8727 function toggleAllowedHTML( input ) {
8728 let b,
8729 id = input.id;
8730 if ( typeof id === 'undefined' || id.indexOf( '-search-input' ) !== -1 ) {
8731 return;
8732 }
8733
8734 jQuery( '#frm-adv-info-tab' ).attr( 'data-fills', id.trim() );
8735 if ( input.classList.contains( 'field_custom_html' ) ) {
8736 id = 'field_custom_html';
8737 }
8738
8739 b = [ 'after_html', 'before_html', 'submit_html', 'field_custom_html' ];
8740 if ( jQuery.inArray( id, b ) >= 0 ) {
8741 jQuery( '.frm_code_list li:not(.show_' + id + ')' ).addClass( 'frm_hidden' );
8742 jQuery( '.frm_code_list li.show_' + id ).removeClass( 'frm_hidden' );
8743 }
8744 }
8745
8746 function toggleKeyID( switchTo, e ) {
8747 e.stopPropagation();
8748 jQuery( '.frm_code_list .frmids, .frm_code_list .frmkeys' ).addClass( 'frm_hidden' );
8749 jQuery( '.frm_code_list .' + switchTo ).removeClass( 'frm_hidden' );
8750 jQuery( '.frmids, .frmkeys' ).removeClass( 'current' );
8751 jQuery( '.' + switchTo ).addClass( 'current' );
8752 }
8753
8754 function onActionLoaded( event ) {
8755 const settings = event.target.closest( '.frm_form_action_settings' );
8756 if ( settings && ( settings.classList.contains( 'frm_single_email_settings' ) || settings.classList.contains( 'frm_single_on_submit_settings' ) ) ) {
8757 initWysiwygOnActionLoaded( settings );
8758 }
8759 }
8760
8761 function initWysiwygOnActionLoaded( settings ) {
8762 settings.querySelectorAll( '.wp-editor-area' ).forEach( wysiwyg => {
8763 frmDom.wysiwyg.init(
8764 wysiwyg,
8765 { height: 160, addFocusEvents: true }
8766 );
8767 });
8768 }
8769
8770 /* Global settings page */
8771 function loadSettingsTab( anchor ) {
8772 const holder = anchor.replace( '#', '' );
8773 const holderContainer = jQuery( '.frm_' + holder + '_ajax' );
8774 if ( holderContainer.length ) {
8775 jQuery.ajax({
8776 type: 'POST', url: ajaxurl,
8777 data: {
8778 'action': 'frm_settings_tab',
8779 'tab': holder.replace( '_settings', '' ),
8780 'nonce': frmGlobal.nonce
8781 },
8782 success: function( html ) {
8783 holderContainer.replaceWith( html );
8784 }
8785 });
8786 }
8787 }
8788
8789 function uninstallNow() {
8790 /*jshint validthis:true */
8791 if ( confirmLinkClick( this ) === true ) {
8792 jQuery( '.frm_uninstall .frm-wait' ).css( 'visibility', 'visible' );
8793 jQuery.ajax({
8794 type: 'POST',
8795 url: ajaxurl,
8796 data: 'action=frm_uninstall&nonce=' + frmGlobal.nonce,
8797 success: function( msg ) {
8798 jQuery( '.frm_uninstall' ).fadeOut( 'slow' );
8799 window.location = msg;
8800 }
8801 });
8802 }
8803 return false;
8804 }
8805
8806 function saveAddonLicense() {
8807 /*jshint validthis:true */
8808 const button = jQuery( this );
8809 const buttonName = this.name;
8810 const pluginSlug = this.getAttribute( 'data-plugin' );
8811 const action = buttonName.replace( 'edd_' + pluginSlug + '_license_', '' );
8812 let license = document.getElementById( 'edd_' + pluginSlug + '_license_key' ).value;
8813 button.get(0).disabled = true;
8814 jQuery.ajax({
8815 type: 'POST', url: ajaxurl, dataType: 'json',
8816 data: {action: 'frm_addon_' + action, license: license, plugin: pluginSlug, nonce: frmGlobal.nonce},
8817 success: function( msg ) {
8818 button.get(0).disabled = false;
8819 const thisRow = button.closest( '.edd_frm_license_row' );
8820 if ( action === 'deactivate' ) {
8821 license = '';
8822 document.getElementById( 'edd_' + pluginSlug + '_license_key' ).value = '';
8823 }
8824 thisRow.find( '.edd_frm_license' ).html( license );
8825 const eddWrapper = button.get(0).closest( '.frm_form_field' );
8826 const actionIsSuccess = msg.success === true;
8827 eddWrapper.querySelector( `.frm_icon_font.frm_action_success` ).classList.toggle( 'frm_hidden', ! actionIsSuccess || action === 'deactivate' );
8828 eddWrapper.querySelector( `.frm_icon_font.frm_action_error` ).classList.toggle( 'frm_hidden', actionIsSuccess);
8829
8830 const messageBox = thisRow.find( '.frm_license_msg' );
8831 messageBox.html( msg.message );
8832 if ( msg.message !== '' ) {
8833 setTimeout( function() {
8834 messageBox.html( '' );
8835 thisRow.find( '.frm_icon_font' ).addClass( 'frm_hidden' );
8836 if ( actionIsSuccess ) {
8837 const actionIsActivate = action === 'activate';
8838 thisRow.get(0).querySelector( '.edd_frm_unauthorized' ).classList.toggle( 'frm_hidden', actionIsActivate );
8839 thisRow.get(0).querySelector( '.edd_frm_authorized' ).classList.toggle( 'frm_hidden', ! actionIsActivate );
8840 }
8841 }, 2000 );
8842 }
8843 }
8844 });
8845 }
8846
8847 /* Import/Export page */
8848
8849 function startFormMigration( event ) {
8850 event.preventDefault();
8851
8852 const checkedBoxes = jQuery( event.target ).find( 'input:checked' );
8853 if ( ! checkedBoxes.length ) {
8854 return;
8855 }
8856
8857 const ids = [];
8858 checkedBoxes.each( function( i ) {
8859 ids[i] = this.value;
8860 });
8861
8862 // Begin the import process.
8863 importForms( ids, event.target );
8864 }
8865
8866 /**
8867 * Begins the process of importing the forms.
8868 */
8869 function importForms( forms, targetForm ) {
8870
8871 // Hide the form select section.
8872 const $form = jQuery( targetForm ),
8873 $processSettings = $form.next( '.frm-importer-process' );
8874
8875 // Display total number of forms we have to import.
8876 $processSettings.find( '.form-total' ).text( forms.length );
8877 $processSettings.find( '.form-current' ).text( '1' );
8878
8879 $form.hide();
8880
8881 // Show processing status.
8882 // '.process-completed' might have been shown earlier during a previous import, so hide now.
8883 $processSettings.find( '.process-completed' ).hide();
8884 $processSettings.show();
8885
8886 // Create global import queue.
8887 s.importQueue = forms;
8888 s.imported = 0;
8889
8890 // Import the first form in the queue.
8891 importForm( $processSettings );
8892 }
8893
8894 /**
8895 * Imports a single form from the import queue.
8896 */
8897 function importForm( $processSettings ) {
8898 const formID = s.importQueue[0],
8899 provider = jQuery( '#welcome-panel' ).find( 'input[name="slug"]' ).val(),
8900 data = {
8901 action: 'frm_import_' + provider,
8902 form_id: formID,
8903 nonce: frmGlobal.nonce
8904 };
8905
8906 // Trigger AJAX import for this form.
8907 jQuery.post( ajaxurl, data, function( res ) {
8908
8909 if ( res.success ) {
8910 let statusUpdate;
8911
8912 if ( res.data.error ) {
8913 statusUpdate = '<p>' + res.data.name + ': ' + res.data.msg + '</p>';
8914 } else {
8915 statusUpdate = '<p>Imported <a href="' + res.data.link + '" target="_blank">' + res.data.name + '</a></p>';
8916 }
8917
8918 $processSettings.find( '.status' ).prepend( statusUpdate );
8919 $processSettings.find( '.status' ).show();
8920
8921 // Remove this form ID from the queue.
8922 s.importQueue = jQuery.grep( s.importQueue, function( value ) {
8923 return value != formID;
8924 });
8925 s.imported++;
8926
8927 if ( s.importQueue.length === 0 ) {
8928 $processSettings.find( '.process-count' ).hide();
8929 $processSettings.find( '.forms-completed' ).text( s.imported );
8930 $processSettings.find( '.process-completed' ).show();
8931 } else {
8932 // Import next form in the queue.
8933 $processSettings.find( '.form-current' ).text( s.imported + 1 );
8934 importForm( $processSettings );
8935 }
8936 }
8937 });
8938 }
8939
8940 function validateExport( e ) {
8941 /*jshint validthis:true */
8942 e.preventDefault();
8943
8944 let s = false;
8945 const $exportForms = jQuery( 'input[name="frm_export_forms[]"]' );
8946
8947 if ( ! jQuery( 'input[name="frm_export_forms[]"]:checked' ).val() ) {
8948 $exportForms.closest( '.frm-table-box' ).addClass( 'frm_blank_field' );
8949 s = 'stop';
8950 }
8951
8952 const $exportType = jQuery( 'input[name="type[]"]' );
8953 if ( ! jQuery( 'input[name="type[]"]:checked' ).val() && $exportType.attr( 'type' ) === 'checkbox' ) {
8954 $exportType.closest( 'p' ).addClass( 'frm_blank_field' );
8955 s = 'stop';
8956 }
8957
8958 if ( s === 'stop' ) {
8959 return false;
8960 }
8961
8962 e.stopPropagation();
8963 this.submit();
8964 }
8965
8966 function removeExportError() {
8967 /*jshint validthis:true */
8968 const t = jQuery( this ).closest( '.frm_blank_field' );
8969 if ( typeof t === 'undefined' ) {
8970 return;
8971 }
8972
8973 const $thisName = this.name;
8974 if ( $thisName === 'type[]' && jQuery( 'input[name="type[]"]:checked' ).val() ) {
8975 t.removeClass( 'frm_blank_field' );
8976 } else if ( $thisName === 'frm_export_forms[]' && jQuery( this ).val() ) {
8977 t.removeClass( 'frm_blank_field' );
8978 }
8979
8980 }
8981
8982 function checkCSVExtension() {
8983 /*jshint validthis:true */
8984 const f = jQuery( this ).val();
8985 const re = /\.csv$/i;
8986 if ( f.match( re ) !== null ) {
8987 jQuery( '.show_csv' ).fadeIn();
8988 } else {
8989 jQuery( '.show_csv' ).fadeOut();
8990 }
8991 }
8992
8993 function getExportOption() {
8994 const exportFormatSelect = document.querySelector( 'select[name="format"]' );
8995 if ( exportFormatSelect ) {
8996 return exportFormatSelect.value;
8997 }
8998 return '';
8999 }
9000
9001 function exportTypeChanged( event ) {
9002 const value = event.target.value;
9003 showOrHideRepeaters( value );
9004 checkExportTypes.call( event.target );
9005 checkSelectedAllFormsCheckbox( value );
9006 }
9007
9008 function checkSelectedAllFormsCheckbox( exportType ) {
9009 const selectAllCheckbox = document.getElementById( 'frm-export-select-all' );
9010 if ( exportType === 'csv' ) {
9011 selectAllCheckbox.checked = false;
9012 selectAllCheckbox.disabled = true;
9013 } else {
9014 selectAllCheckbox.disabled = false;
9015 }
9016 }
9017
9018 function checkExportTypes() {
9019 /*jshint validthis:true */
9020 const $dropdown = jQuery( this );
9021 const $selected = $dropdown.find( ':selected' );
9022 const s = $selected.data( 'support' );
9023
9024 const multiple = s.indexOf( '|' );
9025 jQuery( 'input[name="type[]"]' ).each( function() {
9026 this.checked = false;
9027 if ( s.indexOf( this.value ) >= 0 ) {
9028 this.disabled = false;
9029 if ( multiple === -1 ) {
9030 this.checked = true;
9031 }
9032 } else {
9033 this.disabled = true;
9034 }
9035 });
9036
9037 if ( $dropdown.val() === 'csv' ) {
9038 jQuery( '.csv_opts' ).show();
9039 jQuery( '.xml_opts' ).hide();
9040 } else {
9041 jQuery( '.csv_opts' ).hide();
9042 jQuery( '.xml_opts' ).show();
9043 }
9044
9045 const c = $selected.data( 'count' );
9046 const exportField = jQuery( 'input[name="frm_export_forms[]"]' );
9047 if ( c === 'single' ) {
9048 exportField.prop( 'multiple', false );
9049 exportField.prop( 'checked', false );
9050 } else {
9051 exportField.prop( 'multiple', true );
9052 exportField.prop( 'disabled', false );
9053 }
9054 $dropdown.trigger( 'change' );
9055 }
9056
9057 function showOrHideRepeaters( exportOption ) {
9058 if ( exportOption === '' ) {
9059 return;
9060 }
9061
9062 const repeaters = document.querySelectorAll( '.frm-is-repeater' );
9063 if ( ! repeaters.length ) {
9064 return;
9065 }
9066
9067 if ( exportOption === 'csv' ) {
9068 repeaters.forEach( form => {
9069 form.classList.remove( 'frm_hidden' );
9070 });
9071 } else {
9072 repeaters.forEach( form => {
9073 form.classList.add( 'frm_hidden' );
9074 });
9075 }
9076
9077 searchContent.call( document.querySelector( '.frm-auto-search' ) );
9078 }
9079
9080 function preventMultipleExport() {
9081 const type = jQuery( 'select[name=format]' ),
9082 selected = type.find( ':selected' ),
9083 count = selected.data( 'count' ),
9084 exportField = jQuery( 'input[name="frm_export_forms[]"]' );
9085
9086 if ( count === 'single' ) {
9087 // Disable all other fields to prevent multiple selections.
9088 if ( this.checked ) {
9089 exportField.prop( 'disabled', true );
9090 this.removeAttribute( 'disabled' );
9091 } else {
9092 exportField.prop( 'disabled', false );
9093 }
9094 } else {
9095 exportField.prop( 'disabled', false );
9096 }
9097 }
9098
9099 function initiateMultiselect() {
9100 jQuery( '.frm_multiselect' ).hide().each( frmDom.bootstrap.multiselect.init );
9101 }
9102
9103 /* Addons page */
9104 function installMultipleAddons( e ) {
9105 e.preventDefault();
9106 toggleAddonState( this, 'frm_multiple_addons' );
9107 }
9108
9109 function activateAddon( e ) {
9110 e.preventDefault();
9111 toggleAddonState( this, 'frm_activate_addon' );
9112 }
9113
9114 function installAddon( e ) {
9115 e.preventDefault();
9116 toggleAddonState( this, 'frm_install_addon' );
9117 }
9118
9119 function toggleAddonState( clicked, action ) {
9120 let button, plugin, el, message;
9121
9122 // Remove any leftover error messages, output an icon and get the plugin basename that needs to be activated.
9123 jQuery( '.frm-addon-error' ).remove();
9124 button = jQuery( clicked );
9125 plugin = button.attr( 'rel' );
9126 el = button.parent();
9127 message = el.parent().find( '.addon-status-label' );
9128
9129 button.addClass( 'frm_loading_button' );
9130
9131 // Process the Ajax to perform the activation.
9132 jQuery.ajax({
9133 url: ajaxurl,
9134 type: 'POST',
9135 async: true,
9136 cache: false,
9137 dataType: 'json',
9138 data: {
9139 action: action,
9140 nonce: frmGlobal.nonce,
9141 plugin: plugin
9142 },
9143 success: function( response ) {
9144 response = response?.data ?? response;
9145
9146 let saveAndReload;
9147
9148 if ( 'string' !== typeof response && 'string' === typeof response.message ) {
9149 if ( 'undefined' !== typeof response.saveAndReload ) {
9150 saveAndReload = response.saveAndReload;
9151 }
9152 response = response.message;
9153 }
9154
9155 const error = extractErrorFromAddOnResponse( response );
9156 if ( error ) {
9157 addonError( error, el, button );
9158 return;
9159 }
9160
9161 afterAddonInstall( response, button, message, el, saveAndReload, action );
9162
9163 /**
9164 * Trigger an action after successfully toggling the addon state.
9165 *
9166 * @param {Object} response
9167 */
9168 wp.hooks.doAction( 'frm_update_addon_state', response );
9169 },
9170 error: function() {
9171 button.removeClass( 'frm_loading_button' );
9172 }
9173 });
9174 }
9175
9176 function installAddonWithCreds( e ) {
9177 // Prevent the default action, let the user know we are attempting to install again and go with it.
9178 e.preventDefault();
9179
9180 // Now let's make another Ajax request once the user has submitted their credentials.
9181 const proceed = jQuery( this );
9182 const el = proceed.parent().parent();
9183 const plugin = proceed.attr( 'rel' );
9184
9185 proceed.addClass( 'frm_loading_button' );
9186
9187 jQuery.ajax({
9188 url: ajaxurl,
9189 type: 'POST',
9190 async: true,
9191 cache: false,
9192 dataType: 'json',
9193 data: {
9194 action: 'frm_install_addon',
9195 nonce: frmAdminJs.nonce,
9196 plugin: plugin,
9197 hostname: el.find( '#hostname' ).val(),
9198 username: el.find( '#username' ).val(),
9199 password: el.find( '#password' ).val()
9200 },
9201 success: function( response ) {
9202 response = response?.data ?? response;
9203
9204 const error = extractErrorFromAddOnResponse( response );
9205 if ( error ) {
9206 addonError( error, el, proceed );
9207 return;
9208 }
9209
9210 afterAddonInstall( response, proceed, message, el );
9211 },
9212 error: function() {
9213 proceed.removeClass( 'frm_loading_button' );
9214 }
9215 });
9216 }
9217
9218 function afterAddonInstall( response, button, message, el, saveAndReload, action = 'frm_activate_addon' ) {
9219 const addonStatuses = document.querySelectorAll( '.frm-addon-status' );
9220 addonStatuses.forEach(
9221 addonStatus => {
9222 addonStatus.textContent = response;
9223 addonStatus.style.display = 'block';
9224 }
9225 );
9226
9227 // The Ajax request was successful, so let's update the output.
9228 button.css({ opacity: '0' });
9229
9230 document.querySelectorAll( '.frm-oneclick' ).forEach(
9231 oneClick => {
9232 oneClick.style.display = 'none';
9233 }
9234 );
9235
9236 jQuery( '#frm_upgrade_modal h2' ).hide();
9237 jQuery( '#frm_upgrade_modal .frm_lock_icon' ).addClass( 'frm_lock_open_icon' );
9238 jQuery( '#frm_upgrade_modal .frm_lock_icon use' ).attr( 'xlink:href', '#frm_lock_open_icon' );
9239
9240 // Proceed with CSS changes
9241 const actionMap = {
9242 frm_activate_addon: { class: 'frm-addon-active', message: frmAdminJs.active },
9243 frm_deactivate_addon: { class: 'frm-addon-installed', message: frmAdminJs.installed },
9244 frm_uninstall_addon: { class: 'frm-addon-not-installed', message: frmAdminJs.not_installed }
9245 };
9246 actionMap.frm_install_addon = actionMap.frm_activate_addon;
9247
9248 const messageElement = message[0];
9249 if ( messageElement ) {
9250 messageElement.textContent = actionMap[action].message;
9251 }
9252
9253 const parentElement = el[0].parentElement;
9254 parentElement.classList.remove( 'frm-addon-not-installed', 'frm-addon-installed', 'frm-addon-active' );
9255 parentElement.classList.add( actionMap[action].class );
9256
9257 const buttonElement = button[0];
9258 buttonElement.classList.remove( 'frm_loading_button' );
9259
9260 // Maybe refresh import and SMTP pages
9261 const refreshPage = document.querySelectorAll( '.frm-admin-page-import, #frm-admin-smtp, #frm-welcome' );
9262 if ( refreshPage.length > 0 ) {
9263 window.location.reload();
9264 return;
9265 }
9266
9267 if ([ 'settings', 'form_builder' ].includes( saveAndReload ) ) {
9268 addonStatuses.forEach(
9269 addonStatus => {
9270 const inModal = null !== addonStatus.closest( '#frm_upgrade_modal' );
9271 addonStatus.appendChild( getSaveAndReloadSettingsOptions( saveAndReload, inModal ) );
9272 }
9273 );
9274 }
9275 }
9276
9277 function getSaveAndReloadSettingsOptions( saveAndReload, inModal ) {
9278 const className = 'frm-save-and-reload-options';
9279 const children = [ saveAndReloadSettingsButton( saveAndReload ) ];
9280 if ( inModal ) {
9281 children.push( closePopupButton() );
9282 }
9283 return div({ className, children });
9284 }
9285
9286 function saveAndReloadSettingsButton( saveAndReload ) {
9287 const button = document.createElement( 'button' );
9288 button.classList.add( 'frm-save-and-reload', 'button', 'button-primary', 'frm-button-primary' );
9289 button.textContent = __( 'Save and Reload', 'formidable' );
9290 button.addEventListener( 'click', () => {
9291 if ( saveAndReload === 'form_builder' ) {
9292 saveAndReloadFormBuilder();
9293 } else if ( saveAndReload === 'settings' ) {
9294 saveAndReloadSettings();
9295 }
9296 });
9297 return button;
9298 }
9299
9300 function closePopupButton() {
9301 const a = document.createElement( 'a' );
9302 a.setAttribute( 'href', '#' );
9303 a.classList.add( 'button', 'button-secondary', 'frm-button-secondary', 'dismiss' );
9304 a.textContent = __( 'Close', 'formidable' );
9305 return a;
9306 }
9307
9308 function extractErrorFromAddOnResponse( response ) {
9309 if ( typeof response !== 'string' ) {
9310 if ( typeof response.success !== 'undefined' && response.success ) {
9311 return false;
9312 }
9313
9314 if ( response.form ) {
9315 if ( jQuery( response.form ).is( '#message' ) ) {
9316 return {
9317 message: jQuery( response.form ).find( 'p' ).html()
9318 };
9319 }
9320 }
9321
9322 return response;
9323 }
9324
9325 return false;
9326 }
9327
9328 function addonError( response, el, button ) {
9329 if ( response.form ) {
9330 jQuery( '.frm-inline-error' ).remove();
9331 button.closest( '.frm-card' )
9332 .html( response.form )
9333 .css({ padding: 5 })
9334 .find( '#upgrade' )
9335 .attr( 'rel', button.attr( 'rel' ) )
9336 .on( 'click', installAddonWithCreds );
9337 } else {
9338 el.append( '<div class="frm-addon-error frm_error_style"><p><strong>' + response.message + '</strong></p></div>' );
9339 button.removeClass( 'frm_loading_button' );
9340 jQuery( '.frm-addon-error' ).delay( 4000 ).fadeOut();
9341 }
9342 }
9343
9344 /* Templates */
9345 function showActiveCampaignForm() {
9346 loadApiEmailForm();
9347 }
9348
9349 function handleApiFormError( inputId, errorId, type, message ) {
9350 const $error = jQuery( errorId );
9351 $error.removeClass( 'frm_hidden' ).attr( 'frm-error', type );
9352
9353 if ( typeof message !== 'undefined' ) {
9354 $error.find( 'span[frm-error="' + type + '"]' ).text( message );
9355 }
9356
9357 jQuery( inputId ).one( 'keyup', function() {
9358 $error.addClass( 'frm_hidden' );
9359 });
9360 }
9361
9362 function handleEmailAddressError( type ) {
9363 handleApiFormError( '#frm_leave_email', '#frm_leave_email_error', type );
9364 }
9365
9366 function loadApiEmailForm() {
9367 const formContainer = document.getElementById( 'frmapi-email-form' );
9368 jQuery.ajax({
9369 dataType: 'json',
9370 url: formContainer.getAttribute( 'data-url' ),
9371 success: function( json ) {
9372 let form = json.renderedHtml;
9373 form = form.replace( /<link\b[^>]*(formidableforms.css|action=frmpro_css)[^>]*>/gi, '' );
9374 formContainer.innerHTML = form;
9375 }
9376 });
9377 }
9378
9379 function initAutocomplete( container ) {
9380 frmDom.autocomplete.initSelectionAutocomplete( container );
9381 }
9382
9383 function nextInstallStep( thisStep ) {
9384 thisStep.classList.add( 'frm_grey' );
9385 thisStep.nextElementSibling.classList.remove( 'frm_grey' );
9386 }
9387
9388 function installTemplateFieldset( e ) {
9389 /*jshint validthis:true */
9390 const fieldset = this.parentNode.parentNode,
9391 action = fieldset.elements.type.value,
9392 button = this;
9393 e.preventDefault();
9394 button.classList.add( 'frm_loading_button' );
9395 installNewForm( fieldset, action, button );
9396 }
9397
9398 function installTemplate( e ) {
9399 /*jshint validthis:true */
9400 const action = this.elements.type.value,
9401 button = this.querySelector( 'button' );
9402 e.preventDefault();
9403 button.classList.add( 'frm_loading_button' );
9404 installNewForm( this, action, button );
9405 }
9406
9407 function installNewForm( form, action, button ) {
9408 const formData = formToData( form );
9409 const formName = formData.template_name;
9410 const formDesc = formData.template_desc;
9411 const link = form.elements.link.value;
9412
9413 let data = {
9414 action: action,
9415 xml: link,
9416 name: formName,
9417 desc: formDesc,
9418 form: JSON.stringify( formData ),
9419 nonce: frmGlobal.nonce
9420 };
9421
9422 const hookName = 'frm_before_install_new_form';
9423 const filterArgs = { formData };
9424 data = wp.hooks.applyFilters( hookName, data, filterArgs );
9425
9426 postAjax( data, function( response ) {
9427 if ( typeof response.redirect !== 'undefined' ) {
9428 const redirect = response.redirect;
9429 if ( typeof form.elements.redirect === 'undefined' ) {
9430 window.location = redirect;
9431 } else {
9432 const href = document.getElementById( 'frm-redirect-link' );
9433 if ( typeof link !== 'undefined' && href !== null ) {
9434 // Show the next installation step.
9435 href.setAttribute( 'href', redirect );
9436 href.classList.remove( 'frm_grey', 'disabled' );
9437 nextInstallStep( form.parentNode.parentNode );
9438 button.classList.add( 'frm_grey', 'disabled' );
9439 }
9440 }
9441 } else {
9442 jQuery( '.spinner' ).css( 'visibility', 'hidden' );
9443
9444 // Show response.message
9445 if ( 'string' === typeof response.message ) {
9446 showInstallFormErrorModal( response.message );
9447 }
9448 }
9449 button.classList.remove( 'frm_loading_button' );
9450 });
9451 }
9452
9453 function showInstallFormErrorModal( message ) {
9454 const modalContent = div( message );
9455 modalContent.style.padding = '20px 40px';
9456 const modal = frmDom.modal.maybeCreateModal(
9457 'frmInstallFormErrorModal',
9458 {
9459 title: __( 'Unable to install template', 'formidable' ),
9460 content: modalContent
9461 }
9462 );
9463 modal.classList.add( 'frm_common_modal' );
9464 }
9465
9466 function handleCaptchaTypeChange( e ) {
9467 const thresholdContainer = document.getElementById( 'frm_captcha_threshold_container' );
9468 if ( thresholdContainer ) {
9469 thresholdContainer.classList.toggle( 'frm_hidden', 'v3' !== e.target.value );
9470 }
9471 }
9472
9473 function trashTemplate( e ) {
9474 /*jshint validthis:true */
9475 const id = this.getAttribute( 'data-id' );
9476 e.preventDefault();
9477
9478 data = {
9479 action: 'frm_forms_trash',
9480 id: id,
9481 nonce: frmGlobal.nonce
9482 };
9483 postAjax( data, function() {
9484 const card = document.getElementById( 'frm-template-custom-' + id );
9485 fadeOut( card, function() {
9486 card.parentNode.removeChild( card );
9487 });
9488 });
9489 }
9490
9491 function searchContent() {
9492 /*jshint validthis:true */
9493 let i,
9494 regEx = false,
9495 searchText = this.value.toLowerCase(),
9496 toSearch = this.getAttribute( 'data-tosearch' ),
9497 items = document.getElementsByClassName( toSearch );
9498
9499 if ( this.tagName === 'SELECT' ) {
9500 searchText = selectedOptions( this );
9501 searchText = searchText.join( '|' ).toLowerCase();
9502 regEx = true;
9503 }
9504
9505 if ( toSearch === 'frm-action' && searchText !== '' ) {
9506 const addons = document.getElementById( 'frm_email_addon_menu' ).classList;
9507 addons.remove( 'frm-all-actions' );
9508 addons.add( 'frm-limited-actions' );
9509 }
9510
9511 for ( i = 0; i < items.length; i++ ) {
9512 const innerText = items[i].innerText.toLowerCase();
9513
9514 const itemCanBeShown = ! ( getExportOption() === 'xml' && items[i].classList.contains( 'frm-is-repeater' ) );
9515 if ( searchText === '' ) {
9516 if ( itemCanBeShown && checkContextualShortcode( items[i] ) ) {
9517 items[i].classList.remove( 'frm_hidden' );
9518 }
9519 items[i].classList.remove( 'frm-search-result' );
9520 } else if ( ( regEx && new RegExp( searchText ).test( innerText ) ) || innerText.indexOf( searchText ) >= 0 || textMatchesPlural( innerText, searchText ) ) {
9521 if ( itemCanBeShown && checkContextualShortcode( items[i] ) ) {
9522 items[i].classList.remove( 'frm_hidden' );
9523 }
9524 items[i].classList.add( 'frm-search-result' );
9525 } else {
9526 items[i].classList.add( 'frm_hidden' );
9527 items[i].classList.remove( 'frm-search-result' );
9528 }
9529 }
9530
9531 // Updates the visibility of category headings based on search results.
9532 updateCatHeadingVisibility();
9533
9534 jQuery( this ).trigger( 'frmAfterSearch' );
9535 }
9536
9537 /**
9538 * Allow a search for "signatures" to still match "signature" for example when searching fields.
9539 *
9540 * @since 6.15
9541 *
9542 * @param {string} text The text in the element we are checking for a match.
9543 * @param {string} searchText The text value that is being searched.
9544 * @return {boolean}
9545 */
9546 function textMatchesPlural( text, searchText ) {
9547 if ( searchText === 's' ) {
9548 // Don't match everything when just "s" is searched.
9549 return false;
9550 }
9551
9552 if ( text[ text.length - 1 ] === 's' ) {
9553 // Do not match something with double s if the text already ends in s.
9554 return false;
9555 }
9556
9557 return ( text + 's' ).indexOf( searchText ) >= 0;
9558 }
9559
9560 /**
9561 * Updates the visibility of category headings based on search results.
9562 * If all associated fields are hidden (indicating no search matches),
9563 * the heading is hidden.
9564 *
9565 * @since 6.4.1
9566 */
9567 function updateCatHeadingVisibility() {
9568 const insertFieldsElement = document.querySelector( '#frm-insert-fields' );
9569 if ( ! insertFieldsElement ) {
9570 return;
9571 }
9572
9573 const headingElements = insertFieldsElement.querySelectorAll( ':scope > .frm-with-line' );
9574 headingElements.forEach( heading => {
9575 const fieldsListElement = heading.nextElementSibling;
9576 if ( ! fieldsListElement ) {
9577 return;
9578 }
9579 const listItemElements = fieldsListElement.querySelectorAll( ':scope > li.frmbutton' );
9580 const allHidden = Array.from( listItemElements ).every( li => li.classList.contains( 'frm_hidden' ) );
9581
9582 // Add or remove class based on `allHidden` condition
9583 heading.classList.toggle( 'frm_hidden', allHidden );
9584 });
9585 }
9586
9587 function stopPropagation( e ) {
9588 e.stopPropagation();
9589 }
9590
9591 /* Helpers */
9592
9593 function selectedOptions( select ) {
9594 let opt,
9595 result = [],
9596 options = select && select.options;
9597
9598 for ( let i = 0, iLen = options.length; i < iLen; i++ ) {
9599 opt = options[i];
9600
9601 if ( opt.selected ) {
9602 result.push( opt.value );
9603 }
9604 }
9605 return result;
9606 }
9607
9608 function triggerEvent( element, event ) {
9609 const evt = document.createEvent( 'HTMLEvents' );
9610 evt.initEvent( event, false, true );
9611 element.dispatchEvent( evt );
9612 }
9613
9614 function postAjax( data, success ) {
9615 let response;
9616
9617 const xmlHttp = new XMLHttpRequest();
9618 const params = typeof data === 'string' ? data : Object.keys( data ).map(
9619 function( k ) {
9620 return encodeURIComponent( k ) + '=' + encodeURIComponent( data[k]);
9621 }
9622 ).join( '&' );
9623
9624 xmlHttp.open( 'post', ajaxurl, true );
9625 xmlHttp.onreadystatechange = function() {
9626 if ( xmlHttp.readyState > 3 && xmlHttp.status == 200 ) {
9627 response = xmlHttp.responseText;
9628 try {
9629 response = JSON.parse( response );
9630 } catch ( e ) {
9631 // The response may not be JSON, so just return it.
9632 }
9633 success( response );
9634 }
9635 };
9636 xmlHttp.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
9637 xmlHttp.setRequestHeader( 'Content-type', 'application/x-www-form-urlencoded' );
9638 xmlHttp.send( params );
9639 return xmlHttp;
9640 }
9641
9642 function fadeOut( element, success ) {
9643 element.classList.add( 'frm-fade' );
9644 setTimeout( success, 1000 );
9645 }
9646
9647 function invisible( classes ) {
9648 jQuery( classes ).css( 'visibility', 'hidden' );
9649 }
9650
9651 function visible( classes ) {
9652 jQuery( classes ).css( 'visibility', 'visible' );
9653 }
9654
9655 function initModal( id, width ) {
9656 const $info = jQuery( id );
9657 if ( ! $info.length ) {
9658 return false;
9659 }
9660
9661 if ( typeof width === 'undefined' ) {
9662 width = '550px';
9663 }
9664
9665 const dialogArgs = {
9666 dialogClass: 'frm-dialog',
9667 modal: true,
9668 autoOpen: false,
9669 closeOnEscape: true,
9670 width: width,
9671 resizable: false,
9672 draggable: false,
9673 open: function() {
9674 jQuery( '.ui-dialog-titlebar' ).addClass( 'frm_hidden' ).removeClass( 'ui-helper-clearfix' );
9675 jQuery( '#wpwrap' ).addClass( 'frm_overlay' );
9676 jQuery( '.frm-dialog' ).removeClass( 'ui-widget ui-widget-content ui-corner-all' );
9677 $info.removeClass( 'ui-dialog-content ui-widget-content' );
9678 bindClickForDialogClose( $info );
9679 },
9680 close: function() {
9681 jQuery( '#wpwrap' ).removeClass( 'frm_overlay' );
9682 jQuery( '.spinner' ).css( 'visibility', 'hidden' );
9683
9684 this.removeAttribute( 'data-option-type' );
9685 const optionType = document.getElementById( 'bulk-option-type' );
9686 if ( optionType ) {
9687 optionType.value = '';
9688 }
9689 }
9690 };
9691
9692 $info.dialog( dialogArgs );
9693
9694 return $info;
9695 }
9696
9697 function toggle( cname, id ) {
9698 if ( id === '#' ) {
9699 const cont = document.getElementById( cname );
9700 const hidden = cont.style.display;
9701 if ( hidden === 'none' ) {
9702 cont.style.display = 'block';
9703 } else {
9704 cont.style.display = 'none';
9705 }
9706 } else {
9707 const vis = cname.is( ':visible' );
9708 if ( vis ) {
9709 cname.hide();
9710 } else {
9711 cname.show();
9712 }
9713 }
9714 }
9715
9716 function removeWPUnload() {
9717 window.onbeforeunload = null;
9718 const w = jQuery( window );
9719 w.off( 'beforeunload.widgets' );
9720 w.off( 'beforeunload.edit-post' );
9721 }
9722
9723 function addMultiselectLabelListener() {
9724 const clickListener = ( e ) => {
9725 if ( 'LABEL' !== e.target.nodeName ) {
9726 return;
9727 }
9728
9729 const labelFor = e.target.getAttribute( 'for' );
9730 if ( ! labelFor ) {
9731 return;
9732 }
9733
9734 const input = document.getElementById( labelFor );
9735 if ( ! input || ! input.nextElementSibling ) {
9736 return;
9737 }
9738
9739 const buttonToggle = input.nextElementSibling.querySelector( 'button.dropdown-toggle.multiselect' );
9740 if ( ! buttonToggle ) {
9741 return;
9742 }
9743
9744 const triggerMultiselectClick = () => buttonToggle.click();
9745 setTimeout( triggerMultiselectClick, 0 );
9746 };
9747 document.addEventListener( 'click', clickListener );
9748 }
9749
9750 function maybeChangeEmbedFormMsg() {
9751 const fieldId = jQuery( this ).closest( '.frm-single-settings' ).data( 'fid' );
9752 let fieldItem = document.getElementById( 'frm_field_id_' + fieldId );
9753 if ( null === fieldItem || 'form' !== fieldItem.dataset.type ) {
9754 return;
9755 }
9756
9757 fieldItem = jQuery( fieldItem );
9758
9759 if ( this.options[ this.selectedIndex ].value ) {
9760 fieldItem.find( '.frm-not-set' )[0].classList.add( 'frm_hidden' );
9761 const embedMsg = fieldItem.find( '.frm-embed-message' );
9762 embedMsg.html( embedMsg.data( 'embedmsg' ) + this.options[ this.selectedIndex ].text );
9763 fieldItem.find( '.frm-embed-field-placeholder' )[0].classList.remove( 'frm_hidden' );
9764 } else {
9765 fieldItem.find( '.frm-not-set' )[0].classList.remove( 'frm_hidden' );
9766 fieldItem.find( '.frm-embed-field-placeholder' )[0].classList.add( 'frm_hidden' );
9767 }
9768 }
9769
9770 function toggleProductType() {
9771 const settings = jQuery( this ).closest( '.frm-single-settings' ),
9772 container = settings.find( '.frmjs_product_choices' ),
9773 heading = settings.find( '.frm_prod_options_heading' ),
9774 currentVal = this.options[ this.selectedIndex ].value;
9775
9776 container.removeClass( 'frm_prod_type_single frm_prod_type_user_def' );
9777 heading.removeClass( 'frm_prod_user_def' );
9778
9779 if ( 'single' === currentVal ) {
9780 container.addClass( 'frm_prod_type_single' );
9781 } else if ( 'user_def' === currentVal ) {
9782 container.addClass( 'frm_prod_type_user_def' );
9783 heading.addClass( 'frm_prod_user_def' );
9784 }
9785 }
9786
9787 /**
9788 * @param {Number | string} fieldId
9789 * @return {boolean} True if the field is a product field.
9790 */
9791 function isProductField( fieldId ) {
9792 const field = document.getElementById( 'frm_field_id_' + fieldId );
9793 if ( field === null ) {
9794 return false;
9795 }
9796 return 'product' === field.getAttribute( 'data-type' );
9797 }
9798
9799 /**
9800 * Serialize form data with vanilla JS.
9801 */
9802 function formToData( form ) {
9803 let subKey, i,
9804 object = {},
9805 formData = form.elements;
9806
9807 for ( i = 0; i < formData.length; i++ ) {
9808 let input = formData[i],
9809 key = input.name,
9810 value = input.value,
9811 names = key.match( /(.*)\[(.*)\]/ );
9812
9813 if ( ( input.type === 'radio' || input.type === 'checkbox' ) && ! input.checked ) {
9814 continue;
9815 }
9816
9817 if ( names !== null ) {
9818 key = names[1];
9819 subKey = names[2];
9820 if ( ! Reflect.has( object, key ) ) {
9821 object[key] = {};
9822 }
9823 object[key][subKey] = value;
9824 continue;
9825 }
9826
9827 // Reflect.has in favor of: object.hasOwnProperty(key)
9828 if ( ! Reflect.has( object, key ) ) {
9829 object[key] = value;
9830 continue;
9831 }
9832 if ( ! Array.isArray( object[key]) ) {
9833 object[key] = [ object[key] ];
9834 }
9835 object[key].push( value );
9836 }
9837
9838 return object;
9839 }
9840
9841 /**
9842 * Show, hide, and sort subfields of Name field on form builder.
9843 *
9844 * @since 4.11
9845 */
9846 function handleNameFieldOnFormBuilder() {
9847 /**
9848 * Gets subfield element from cache.
9849 *
9850 * @param {String} fieldId Field ID.
9851 * @param {String} key Cache key.
9852 * @returns {HTMLElement|undefined} Return the element from cache or undefined if not found.
9853 */
9854 const getSubFieldElFromCache = ( fieldId, key ) => {
9855 window.frmCachedSubFields = window.frmCachedSubFields || {};
9856 window.frmCachedSubFields[fieldId] = window.frmCachedSubFields[fieldId] || {};
9857 return window.frmCachedSubFields[fieldId][key];
9858 };
9859
9860 /**
9861 * Sets subfield element to cache.
9862 *
9863 * @param {String} fieldId Field ID.
9864 * @param {String} key Cache key.
9865 * @param {HTMLElement} el Element.
9866 */
9867 const setSubFieldElToCache = ( fieldId, key, el ) => {
9868 window.frmCachedSubFields = window.frmCachedSubFields || {};
9869 window.frmCachedSubFields[fieldId] = window.frmCachedSubFields[fieldId] || {};
9870 window.frmCachedSubFields[fieldId][key] = el;
9871 };
9872
9873 /**
9874 * Gets column class from the number of columns.
9875 *
9876 * @param {Number} colCount Number of columns.
9877 * @returns {string}
9878 */
9879 const getColClass = colCount => 'frm' + parseInt( 12 / colCount );
9880
9881 const colClasses = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ].map( num => 'frm' + num );
9882
9883 const allSubFieldNames = [ 'first', 'middle', 'last' ];
9884
9885 /**
9886 * Handles name layout change.
9887 *
9888 * @param {Event} event Event object.
9889 */
9890 const onChangeLayout = event => {
9891 const value = event.target.value;
9892 const subFieldNames = value.split( '_' );
9893 const fieldId = event.target.dataset.fieldId;
9894
9895 /*
9896 * Live update form on the form builder.
9897 */
9898 const container = document.querySelector( '#field_' + fieldId + '_inner_container .frm_combo_inputs_container' );
9899 const newColClass = getColClass( subFieldNames.length );
9900
9901 // Set all sub field elements to cache and hide all of them first.
9902 allSubFieldNames.forEach( name => {
9903 const subFieldEl = container.querySelector( '[data-sub-field-name="' + name + '"]' );
9904 if ( subFieldEl ) {
9905 subFieldEl.classList.add( 'frm_hidden' );
9906 subFieldEl.classList.remove( ...colClasses );
9907 setSubFieldElToCache( fieldId, name, subFieldEl );
9908 }
9909 });
9910
9911 subFieldNames.forEach( subFieldName => {
9912 const subFieldEl = getSubFieldElFromCache( fieldId, subFieldName );
9913 if ( ! subFieldEl ) {
9914 return;
9915 }
9916
9917 subFieldEl.classList.remove( 'frm_hidden' );
9918 subFieldEl.classList.add( newColClass );
9919
9920 container.append( subFieldEl );
9921 });
9922
9923 /*
9924 * Live update subfield options.
9925 */
9926 // Hide all subfield options.
9927 allSubFieldNames.forEach( name => {
9928 const optionsEl = document.querySelector( '.frm_sub_field_options-' + name + '[data-field-id="' + fieldId + '"]' );
9929 if ( optionsEl ) {
9930 optionsEl.classList.add( 'frm_hidden' );
9931 setSubFieldElToCache( fieldId, name + '_options', optionsEl );
9932 }
9933 });
9934
9935 subFieldNames.forEach( subFieldName => {
9936 const optionsEl = getSubFieldElFromCache( fieldId, subFieldName + '_options' );
9937 if ( ! optionsEl ) {
9938 return;
9939 }
9940 optionsEl.classList.remove( 'frm_hidden' );
9941 });
9942 };
9943
9944 const dropdownSelector = '.frm_name_layout_dropdown';
9945 document.addEventListener( 'change', event => {
9946 if ( event.target.matches( dropdownSelector ) ) {
9947 onChangeLayout( event );
9948 }
9949 }, false );
9950 }
9951
9952 function debounce( func, wait = 100 ) {
9953 return frmDom.util.debounce( func, wait );
9954 }
9955
9956 function addSaveAndDragIconsToOption( fieldId, liObject ) {
9957 let li, useTag, useTagHref;
9958 let hasDragIcon = false;
9959 let hasSaveIcon = false;
9960
9961 if ( liObject.newOption ) {
9962 const parser = new DOMParser();
9963 li = parser.parseFromString( liObject.newOption, 'text/html' ).body.childNodes[0];
9964 } else {
9965 li = liObject;
9966 }
9967
9968 const liIcons = li.querySelectorAll( 'svg' );
9969
9970 liIcons.forEach( ( svg, key ) => {
9971 useTag = svg.getElementsByTagNameNS( 'http://www.w3.org/2000/svg', 'use' )[0];
9972 if ( ! useTag ) {
9973 return;
9974 }
9975 useTagHref = useTag.getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' ) || useTag.getAttribute( 'href' );
9976
9977 if ( useTagHref === '#frm_drag_icon' ) {
9978 hasDragIcon = true;
9979 }
9980
9981 if ( useTagHref === '#frm_save_icon' ) {
9982 hasSaveIcon = true;
9983 }
9984 });
9985
9986 if ( ! hasDragIcon ) {
9987 li.prepend( icons.drag.cloneNode( true ) );
9988 }
9989
9990 if ( li.querySelector( `[id^=field_key_${fieldId}-]` ) && ! hasSaveIcon ) {
9991 li.querySelector( `[id^=field_key_${fieldId}-]` ).after( icons.save.cloneNode( true ) );
9992 }
9993
9994 if ( liObject.newOption ) {
9995 liObject.newOption = li;
9996 }
9997 }
9998
9999 function maybeAddSaveAndDragIcons( fieldId ) {
10000 fieldOptions = document.querySelectorAll( `[id^=frm_delete_field_${fieldId}-]` );
10001 // return if there are no options.
10002 if ( fieldOptions.length < 2 ) {
10003 return;
10004 }
10005
10006 const options = [ ...fieldOptions ].slice( 1 );
10007 options.forEach( ( li, _key ) => {
10008 if ( li.classList.contains( 'frm_other_option' ) ) {
10009 return;
10010 }
10011 addSaveAndDragIconsToOption( fieldId, li );
10012 });
10013 }
10014
10015 function initOnSubmitAction() {
10016 const onChangeType = event => {
10017 if ( ! event.target.checked ) {
10018 return;
10019 }
10020
10021 const actionEl = event.target.closest( '.frm_form_action_settings' );
10022 actionEl.querySelectorAll( '.frm_on_submit_dependent_setting:not(.frm_hidden)' ).forEach( el => {
10023 el.classList.add( 'frm_hidden' );
10024 });
10025
10026 const activeEls = actionEl.querySelectorAll( '.frm_on_submit_dependent_setting[data-show-if-' + event.target.value + ']' );
10027 activeEls.forEach( activeEl => {
10028 activeEl.classList.remove( 'frm_hidden' );
10029 });
10030
10031 actionEl.setAttribute( 'data-on-submit-type', event.target.value );
10032 };
10033
10034 frmDom.util.documentOn( 'change', '.frm_on_submit_type input[type="radio"]', onChangeType );
10035 }
10036
10037 /**
10038 * Listen for click events for an API-loaded email collection form.
10039 *
10040 * This is used for the Active Campaign sign-up form in the inbox page (when there are no messages).
10041 */
10042 function initAddMyEmailAddress() {
10043 jQuery( document ).on(
10044 'click',
10045 '#frm-add-my-email-address',
10046 event => {
10047 event.preventDefault();
10048 addMyEmailAddress();
10049 }
10050 );
10051
10052 const emptyInbox = document.getElementById( 'frm_empty_inbox' );
10053 const leaveEmailInput = document.getElementById( 'frm_leave_email' );
10054
10055 if ( emptyInbox && leaveEmailInput ) {
10056 const leaveEmailModal = document.getElementById( 'frm-leave-email-modal' );
10057 leaveEmailModal.classList.remove( 'frm_hidden' );
10058 leaveEmailModal.querySelector( '.frm_modal_footer' ).classList.add( 'frm_hidden' );
10059
10060 leaveEmailInput.addEventListener(
10061 'keyup',
10062 event => {
10063 if ( 'Enter' === event.key ) {
10064 const button = document.getElementById( 'frm-add-my-email-address' );
10065 if ( button ) {
10066 button.click();
10067 }
10068 }
10069 }
10070 );
10071 }
10072 }
10073
10074 function addMyEmailAddress() {
10075 const email = document.getElementById( 'frm_leave_email' ).value.trim();
10076 if ( '' === email ) {
10077 handleEmailAddressError( 'empty' );
10078 return;
10079 }
10080
10081 const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
10082 if ( regex.test( email ) === false ) {
10083 handleEmailAddressError( 'invalid' );
10084 return;
10085 }
10086
10087 const $hiddenForm = jQuery( '#frmapi-email-form' ).find( 'form' );
10088 const $hiddenEmailField = $hiddenForm.find( '[type="email"]' ).not( '.frm_verify' );
10089 if ( ! $hiddenEmailField.length ) {
10090 return;
10091 }
10092
10093 const emptyInbox = document.getElementById( 'frm_empty_inbox' );
10094 if ( emptyInbox ) {
10095 document.getElementById( 'frm-add-my-email-address' ).remove();
10096
10097 const emailWrapper = document.getElementById( 'frm_leave_email_wrapper' );
10098 if ( emailWrapper ) {
10099 emailWrapper.classList.add( 'frm_hidden' );
10100 const spinner = span({ className: 'frm-wait frm_spinner' });
10101 spinner.style.visibility = 'visible';
10102 spinner.style.float = 'none';
10103 spinner.style.width = 'unset';
10104 emailWrapper.parentElement.insertBefore(
10105 spinner,
10106 emailWrapper.nextElementSibling
10107 );
10108 }
10109 }
10110
10111 $hiddenEmailField.val( email );
10112 jQuery.ajax({
10113 type: 'POST',
10114 url: $hiddenForm.attr( 'action' ),
10115 data: $hiddenForm.serialize() + '&action=frm_forms_preview'
10116 }).done( function( data ) {
10117 const message = jQuery( data ).find( '.frm_message' ).text().trim();
10118 if ( message.indexOf( 'Thanks!' ) === -1 ) {
10119 handleEmailAddressError( 'invalid' );
10120 return;
10121 }
10122
10123 const apiForm = document.getElementById( 'frmapi-email-form' );
10124 const spinner = apiForm.parentElement.querySelector( '.frm_spinner' );
10125 if ( spinner ) {
10126 spinner.remove();
10127 }
10128
10129 const showSuccessMessage = wp.hooks.applyFilters( 'frm_thank_you_on_signup', true );
10130 if ( showSuccessMessage ) {
10131 // Handle successful form submission.
10132 // handle the Active Campaign form on the inbox page.
10133 document.getElementById( 'frm_leave_email_wrapper' ).replaceWith(
10134 span( __( 'Thank you for signing up!', 'formidable' ) )
10135 );
10136 }
10137 });
10138 }
10139
10140 /**
10141 * Adds footer links to the admin body content.
10142 *
10143 * @return {void}
10144 */
10145 function addAdminFooterLinks() {
10146 const footerLinks = document.querySelector( '.frm-admin-footer-links' );
10147 const container = document.querySelector( '.frm_page_container' ) ?? document.getElementById( 'wpbody-content' );
10148
10149 if ( ! footerLinks || ! container ) {
10150 return;
10151 }
10152
10153 container.appendChild( footerLinks );
10154 footerLinks.classList.remove( 'frm_hidden' );
10155 }
10156
10157 /**
10158 * Apply zebra striping to a table while ignoring empty rows.
10159 *
10160 * @param {string} tableSelector The CSS selector for the table.
10161 * @param {string} emptyRowClass The class name used to identify empty rows.
10162 */
10163 function applyZebraStriping( tableSelector, emptyRowClass ) {
10164 // Get all non-empty table rows within the specified table
10165 const rows = document.querySelectorAll( `${tableSelector} tr${emptyRowClass ? `:not(.${emptyRowClass})` : ''}` );
10166 if ( rows.length < 1 ) {
10167 return;
10168 }
10169
10170 let isOdd = true;
10171 rows.forEach( row => {
10172 // Clean old "frm-odd" or "frm-even" classes and add the appropriate new class
10173 row.classList.remove( 'frm-odd', 'frm-even' );
10174 row.classList.add( isOdd ? 'frm-odd' : 'frm-even' );
10175
10176 isOdd = ! isOdd;
10177 });
10178
10179 const tables = document.querySelectorAll( tableSelector );
10180 tables.forEach( table => table.classList.add( 'frm-zebra-striping' ) );
10181 };
10182
10183 function maybeHideShortcodes( e ) {
10184 if ( ! builderPage ) {
10185 e.stopPropagation();
10186 }
10187
10188 if ( e.target.classList.contains( 'frm-show-box' ) || ( e.target.parentElement && e.target.parentElement.classList.contains( 'frm-show-box' ) ) ) {
10189 return;
10190 }
10191
10192 const sidebar = document.getElementById( 'frm_adv_info' );
10193 if ( ! sidebar ) {
10194 return;
10195 }
10196
10197 if ( sidebar.dataset.fills === e.target.id && typeof e.target.id !== 'undefined' ) {
10198 return;
10199 }
10200
10201 const isChild = e.target.closest( '#frm_adv_info' );
10202
10203 if ( ! isChild && sidebar.style.display !== 'none' ) {
10204 hideShortcodes( sidebar );
10205 }
10206 }
10207
10208 /**
10209 * Initializes and manages the visibility of dependent elements based on the selected options in dropdowns with the 'frm_select_with_dependency' class.
10210 * It sets up initial visibility at page load and updates it on each dropdown change.
10211 *
10212 * @since 6.9
10213 *
10214 * @return {void}
10215 */
10216 function initSelectDependencies() {
10217 const selects = document.querySelectorAll( 'select.frm_select_with_dependency' );
10218
10219 /**
10220 * Toggles the visibility of dependent elements associated with a select element based on its current selection.
10221 *
10222 * @since 6.9
10223 *
10224 * @param {HTMLElement} select The select element whose dependencies need to be managed.
10225 * @return {void}
10226 */
10227 function toggleDependencyVisibility( select ) {
10228 const selectedOption = select.options[ select.selectedIndex ];
10229 select.querySelectorAll( 'option[data-dependency]:not([data-dependency-skip])' ).forEach( option => {
10230 const dependencyElement = document.querySelector( option.dataset.dependency );
10231 dependencyElement?.classList.toggle( 'frm_hidden', selectedOption !== option );
10232 });
10233 }
10234
10235 // Initial setup: Show dependencies based on the current selection in each dropdown
10236 selects.forEach( toggleDependencyVisibility );
10237
10238 // Update dependencies visibility on dropdown change
10239 frmDom.util.documentOn( 'change', 'select.frm_select_with_dependency', ( event ) => toggleDependencyVisibility( event.target ) );
10240 }
10241
10242 /**
10243 * Moves the focus to the next single option input field in the list and positions the cursor at the end of the text.
10244 *
10245 * @param {HTMLElement} currentInput The currently focused input element.
10246 */
10247 function focusNextSingleOptionInput( currentInput ) {
10248 const optionsList = currentInput.closest( '.frm_single_option' ).parentElement;
10249 const inputs = optionsList.querySelectorAll( '.frm_single_option input[name^="field_options[" ], .frm_single_option input[name^="rows_"]' );
10250 const inputsArray = Array.from( inputs );
10251
10252 // Find the index of the currently focused input
10253 const currentIndex = inputsArray.indexOf( currentInput );
10254
10255 if ( currentIndex < 0 ) {
10256 return;
10257 }
10258
10259 // Find the next visible input field
10260 const nextInput = inputsArray.slice( currentIndex + 1 ).find( input => input.offsetParent !== null );
10261
10262 if ( nextInput ) {
10263 nextInput.focus();
10264
10265 // Move the cursor to the end of the text in the next input field
10266 const textLength = nextInput.value.length;
10267 nextInput.setSelectionRange( textLength, textLength );
10268 }
10269 }
10270
10271 return {
10272 init: function() {
10273 initAddMyEmailAddress();
10274 addAdminFooterLinks();
10275
10276 s = {};
10277
10278 // Bootstrap dropdown button
10279 jQuery( '.wp-admin' ).on( 'click', function( e ) {
10280 const t = jQuery( e.target );
10281 const $openDrop = jQuery( '.dropdown.open' );
10282 if ( $openDrop.length && ! t.hasClass( 'dropdown' ) && ! t.closest( '.dropdown' ).length ) {
10283 $openDrop.removeClass( 'open' );
10284 }
10285 });
10286 jQuery( '#frm_bs_dropdown:not(.open) a' ).on( 'click', focusSearchBox );
10287
10288 if ( typeof thisFormId === 'undefined' ) {
10289 thisFormId = jQuery( document.getElementById( 'form_id' ) ).val();
10290 }
10291
10292 // Add event listener for dismissible warning messages.
10293 document.querySelectorAll( '.frm-warning-dismiss' ).forEach( ( dismissIcon ) => {
10294 onClickPreventDefault( dismissIcon, dismissWarningMessage );
10295 });
10296
10297 frmAdminBuild.inboxBannerInit();
10298
10299 if ( $newFields.length > 0 ) {
10300 // only load this on the form builder page
10301 frmAdminBuild.buildInit();
10302 } else if ( document.getElementById( 'frm_notification_settings' ) !== null ) {
10303 // only load on form settings page
10304 frmAdminBuild.settingsInit();
10305 } else if ( document.getElementById( 'frm_styling_form' ) !== null ) {
10306 // load styling settings js
10307 frmAdminBuild.styleInit();
10308 } else if ( document.getElementById( 'form_global_settings' ) !== null ) {
10309 // global settings page
10310 frmAdminBuild.globalSettingsInit();
10311 } else if ( document.getElementById( 'frm_export_xml' ) !== null ) {
10312 // import/export page
10313 frmAdminBuild.exportInit();
10314 } else if ( null !== document.querySelector( '.frm-inbox-wrapper' ) ) {
10315 // Dashboard page inbox.
10316 frmAdminBuild.inboxInit();
10317 } else if ( document.getElementById( 'frm-welcome' ) !== null ) {
10318 // Solution install page
10319 frmAdminBuild.solutionInit();
10320 } else {
10321 initAutocomplete();
10322
10323 jQuery( '[data-frmprint]' ).on( 'click', function() {
10324 window.print();
10325 return false;
10326 });
10327 }
10328
10329 jQuery( document ).on( 'change', 'select[data-toggleclass], input[data-toggleclass]', toggleFormOpts );
10330 initSelectDependencies();
10331
10332 const $advInfo = jQuery( document.getElementById( 'frm_adv_info' ) );
10333 if ( $advInfo.length > 0 || jQuery( '.frm_field_list' ).length > 0 ) {
10334 // only load on the form, form settings, and view settings pages
10335 frmAdminBuild.panelInit();
10336 }
10337
10338 loadTooltips();
10339 initUpgradeModal();
10340
10341 // used on build, form settings, and view settings
10342 const $shortCodeDiv = jQuery( document.getElementById( 'frm_shortcodediv' ) );
10343 if ( $shortCodeDiv.length > 0 ) {
10344 jQuery( 'a.edit-frm_shortcode' ).on( 'click', function() {
10345 if ( $shortCodeDiv.is( ':hidden' ) ) {
10346 $shortCodeDiv.slideDown( 'fast' );
10347 this.style.display = 'none';
10348 }
10349 return false;
10350 });
10351
10352 jQuery( '.cancel-frm_shortcode', '#frm_shortcodediv' ).on( 'click', function() {
10353 $shortCodeDiv.slideUp( 'fast' );
10354 $shortCodeDiv.siblings( 'a.edit-frm_shortcode' ).show();
10355 return false;
10356 });
10357 }
10358
10359 // tabs
10360 jQuery( document ).on( 'click', '#frm-nav-tabs a', clickNewTab );
10361 jQuery( '.post-type-frm_display .frm-nav-tabs a, .frm-category-tabs a' ).on( 'click', function() {
10362 const showUpgradeTab = this.classList.contains( 'frm_show_upgrade_tab' );
10363 if ( this.classList.contains( 'frm_noallow' ) && ! showUpgradeTab ) {
10364 return;
10365 }
10366
10367 if ( showUpgradeTab ) {
10368 populateUpgradeTab( this );
10369 }
10370
10371 clickTab( this );
10372 return false;
10373 });
10374 clickTab( jQuery( '.starttab a' ), 'auto' );
10375
10376 // submit the search form with dropdown
10377 jQuery( document ).on( 'click', '#frm-fid-search-menu a', function() {
10378 const val = this.id.replace( 'fid-', '' );
10379 jQuery( 'select[name="fid"]' ).val( val );
10380 triggerSubmit( document.getElementById( 'posts-filter' ) );
10381 return false;
10382 });
10383
10384 jQuery( '.frm_select_box' ).on( 'click focus', function() {
10385 this.select();
10386 });
10387
10388 jQuery( document ).on( 'input search change', '.frm-auto-search:not(#frm-form-templates-page #template-search-input)', searchContent );
10389 jQuery( document ).on( 'focusin click', '.frm-auto-search', stopPropagation );
10390 const autoSearch = jQuery( '.frm-auto-search' );
10391 if ( autoSearch.val() !== '' ) {
10392 autoSearch.trigger( 'keyup' );
10393 }
10394
10395 // Initialize Formidable Connection.
10396 FrmFormsConnect.init();
10397
10398 jQuery( document ).on( 'click', '.frm-install-addon', installAddon );
10399 jQuery( document ).on( 'click', '.frm-activate-addon', activateAddon );
10400 jQuery( document ).on( 'click', '.frm-solution-multiple', installMultipleAddons );
10401
10402 // prevent annoying confirmation message from WordPress
10403 jQuery( 'button, input[type=submit]' ).on( 'click', removeWPUnload );
10404
10405 addMultiselectLabelListener();
10406
10407 frmAdminBuild.hooks.addFilter(
10408 'frm_before_embed_modal',
10409 ( ids, { element, type }) => {
10410 if ( 'form' !== type ) {
10411 return ids;
10412 }
10413
10414 let formId, formKey;
10415 const row = element.closest( 'tr' );
10416
10417 if ( row ) {
10418 // Embed icon on form index.
10419 formId = parseInt( row.querySelector( '.column-id' ).textContent );
10420 formKey = row.querySelector( '.column-form_key' ).textContent;
10421 } else {
10422 // Embed button in form builder / form settings.
10423 formId = document.getElementById( 'form_id' ).value;
10424
10425 const formKeyInput = document.getElementById( 'frm_form_key' );
10426 if ( formKeyInput ) {
10427 formKey = formKeyInput.value;
10428 } else {
10429 const previewDrop = document.getElementById( 'frm-previewDrop' );
10430 if ( previewDrop ) {
10431 formKey = previewDrop.nextElementSibling.querySelector( '.dropdown-item a' ).getAttribute( 'href' ).split( 'form=' )[1];
10432 }
10433 }
10434 }
10435
10436 return [ formId, formKey ];
10437 }
10438 );
10439
10440 document.querySelectorAll( '#frm-show-fields > li, .frm_grid_container li' ).forEach( ( el, _key ) => {
10441 el.addEventListener( 'click', function() {
10442 const fieldId = this.querySelector( 'li' )?.dataset.fid || this.dataset.fid;
10443 maybeAddSaveAndDragIcons( fieldId );
10444 });
10445 });
10446
10447 const smallScreenProceedButton = document.getElementById( 'frm_small_screen_proceed_button' );
10448 if ( smallScreenProceedButton ) {
10449 onClickPreventDefault( smallScreenProceedButton, () => {
10450 document.getElementById( 'frm_small_device_message_container' )?.remove();
10451 doJsonPost( 'small_screen_proceed', new FormData() );
10452 });
10453 }
10454
10455 const saleBanner = document.getElementById( 'frm_sale_banner' );
10456 const saleDismiss = saleBanner?.querySelector( '.dismiss' );
10457 if ( saleBanner ) {
10458 onClickPreventDefault( saleBanner, ( event ) => {
10459 const target = event.target;
10460 if ( target.closest( '.dismiss' ) ) {
10461 return;
10462 }
10463 window.location.href = saleBanner.getAttribute( 'data-url' );
10464 });
10465
10466 if ( saleDismiss ) {
10467 onClickPreventDefault( saleDismiss, () => {
10468 saleBanner.remove();
10469
10470 const formData = new FormData();
10471 doJsonPost( 'sale_banner_dismiss', formData );
10472 });
10473 }
10474 }
10475 },
10476
10477 buildInit: function() {
10478 jQuery( '#frm_builder_page' ).on( 'mouseup', '*:not(.frm-show-box)', maybeHideShortcodes );
10479
10480 let loadFieldId, $builderForm, builderArea;
10481
10482 debouncedSyncAfterDragAndDrop = debounce( syncAfterDragAndDrop, 10 );
10483 postBodyContent = document.getElementById( 'post-body-content' );
10484 $postBodyContent = jQuery( postBodyContent );
10485
10486 if ( jQuery( '.frm_field_loading' ).length ) {
10487 loadFieldId = jQuery( '.frm_field_loading' ).first().attr( 'id' );
10488 loadFields( loadFieldId );
10489 }
10490
10491 setupSortable( 'ul.frm_sorting' );
10492
10493 document.querySelectorAll( '.field_type_list > li:not(.frm_show_upgrade)' ).forEach( makeDraggable );
10494
10495 jQuery( 'ul.field_type_list, .field_type_list li, ul.frm_code_list, .frm_code_list li, .frm_code_list li a, #frm_adv_info #category-tabs li, #frm_adv_info #category-tabs li a' ).disableSelection();
10496
10497 jQuery( '.frm_submit_ajax' ).on( 'click', submitBuild );
10498 jQuery( '.frm_submit_no_ajax' ).on( 'click', submitNoAjax );
10499
10500 addFormNameModalEvents();
10501
10502 jQuery( 'a.edit-form-status' ).on( 'click', slideDown );
10503 jQuery( '.cancel-form-status' ).on( 'click', slideUp );
10504 jQuery( '.save-form-status' ).on( 'click', function() {
10505 const newStatus = jQuery( document.getElementById( 'form_change_status' ) ).val();
10506 jQuery( 'input[name="new_status"]' ).val( newStatus );
10507 jQuery( document.getElementById( 'form-status-display' ) ).html( newStatus );
10508 jQuery( '.cancel-form-status' ).trigger( 'click' );
10509 return false;
10510 });
10511
10512 jQuery( '.frm_form_builder form' ).first().on( 'submit', function() {
10513 jQuery( '.inplace_field' ).trigger( 'blur' );
10514 });
10515
10516 initiateMultiselect();
10517 renumberPageBreaks();
10518
10519 $builderForm = jQuery( builderForm );
10520 builderArea = document.getElementById( 'frm_form_editor_container' );
10521 $builderForm.on( 'click', '.frm_add_logic_row', addFieldLogicRow );
10522 $builderForm.on( 'click', '.frm_add_watch_lookup_row', addWatchLookupRow );
10523 $builderForm.on( 'change', '.frm_get_values_form', updateGetValueFieldSelection );
10524 $builderForm.on( 'change', '.frm_logic_field_opts', getFieldValues );
10525 $builderForm.on( 'frm-multiselect-changed', 'select[name^="field_options[admin_only_"]', adjustVisibilityValuesForEveryoneValues );
10526
10527 jQuery( document.getElementById( 'frm-insert-fields' ) ).on( 'click', '.frm_add_field', addFieldClick );
10528 $newFields.on( 'click', '.frm_clone_field', duplicateField );
10529 $builderForm.on( 'blur', 'input[id^="frm_calc"]', checkCalculationCreatedByUser );
10530 $builderForm.on( 'change', 'input.frm_format_opt, input.frm_max_length_opt', toggleInvalidMsg );
10531 $builderForm.on( 'change click', '[data-changeme]', liveChanges );
10532 $builderForm.on( 'click', 'input.frm_req_field', markRequired );
10533 $builderForm.on( 'click', '.frm_mark_unique', markUnique );
10534
10535 $builderForm.on( 'change', '.frm_repeat_format', toggleRepeatButtons );
10536 $builderForm.on( 'change', '.frm_repeat_limit', checkRepeatLimit );
10537 $builderForm.on( 'change', '.frm_js_checkbox_limit', checkCheckboxSelectionsLimit );
10538 $builderForm.on( 'input', 'input[name^="field_options[add_label_"]', function() {
10539 updateRepeatText( this, 'add' );
10540 });
10541 $builderForm.on( 'input', 'input[name^="field_options[remove_label_"]', function() {
10542 updateRepeatText( this, 'remove' );
10543 });
10544 $builderForm.on( 'change', 'select[name^="field_options[data_type_"]', maybeClearWatchFields );
10545 jQuery( builderArea ).on( 'click', '.frm-collapse-page', maybeCollapsePage );
10546 jQuery( builderArea ).on( 'click', '.frm-collapse-section', maybeCollapseSection );
10547 $builderForm.on( 'click', '.frm-single-settings h3', maybeCollapseSettings );
10548 $builderForm.on( 'keydown', '.frm-single-settings h3', function( event ) {
10549 // If so, only proceed if the key pressed was 'Enter' or 'Space'
10550 if ( event.key === 'Enter' || event.key === ' ' ) {
10551 event.preventDefault();
10552 maybeCollapseSettings.call( this, event );
10553 }
10554 });
10555
10556 jQuery( builderArea ).on( 'show.bs.dropdown hide.bs.dropdown', changeSectionStyle );
10557
10558 $builderForm.on( 'click', '.frm_toggle_sep_values', toggleSepValues );
10559 $builderForm.on( 'click', '.frm_toggle_image_options', toggleImageOptions );
10560 $builderForm.on( 'click', '.frm_remove_image_option', removeImageFromOption );
10561 $builderForm.on( 'click', '.frm_choose_image_box', addImageToOption );
10562 $builderForm.on( 'change', '.frm_hide_image_text', refreshOptionDisplay );
10563 $builderForm.on( 'change', '.frm_field_options_image_size', setImageSize );
10564 $builderForm.on( 'click', '.frm_multiselect_opt', toggleMultiselect );
10565 $newFields.on( 'mousedown', 'input, textarea, select', stopFieldFocus );
10566 $newFields.on( 'click', 'input[type=radio], input[type=checkbox]', stopFieldFocus );
10567 $newFields.on( 'click', '.frm_delete_field', clickDeleteField );
10568 $newFields.on( 'click', '.frm_select_field', clickSelectField );
10569 jQuery( document ).on( 'click', '.frm_delete_field_group', clickDeleteFieldGroup );
10570 jQuery( document ).on( 'click', '.frm_clone_field_group', duplicateFieldGroup );
10571 jQuery( document ).on( 'click', '#frm_field_group_controls > span:first-child', clickFieldGroupLayout );
10572 jQuery( document ).on( 'click', '.frm-row-layout-option', handleFieldGroupLayoutOptionClick );
10573 jQuery( document ).on( 'click', '.frm-merge-fields-into-row .frm-row-layout-option', handleFieldGroupLayoutOptionInsideMergeClick );
10574 jQuery( document ).on( 'click', '.frm-custom-field-group-layout', customFieldGroupLayoutClick );
10575 jQuery( document ).on( 'click', '.frm-merge-fields-into-row .frm-custom-field-group-layout', customFieldGroupLayoutInsideMergeClick );
10576 jQuery( document ).on( 'click', '.frm-break-field-group', breakFieldGroupClick );
10577 $newFields.on( 'click', '#frm_field_group_popup .frm_grid_container input', focusFieldGroupInputOnClick );
10578 jQuery( document ).on( 'click', '.frm-cancel-custom-field-group-layout', cancelCustomFieldGroupClick );
10579 jQuery( document ).on( 'click', '.frm-save-custom-field-group-layout', saveCustomFieldGroupClick );
10580 $newFields.on( 'click', 'ul.frm_sorting', fieldGroupClick );
10581 jQuery( document ).on( 'click', '.frm-merge-fields-into-row', mergeFieldsIntoRowClick );
10582 jQuery( document ).on( 'click', '.frm-delete-field-groups', deleteFieldGroupsClick );
10583 $newFields.on( 'click', '.frm-field-action-icons [data-toggle="dropdown"]', function() {
10584 this.closest( 'li.form-field' ).classList.add( 'frm-field-settings-open' );
10585 jQuery( document ).on( 'click', '#frm_builder_page', handleClickOutsideOfFieldSettings );
10586 });
10587 $newFields.on( 'mousemove', 'ul.frm_sorting', checkForMultiselectKeysOnMouseMove );
10588 $newFields.on( 'show.bs.dropdown', '.frm-field-action-icons', onFieldActionDropdownShow );
10589 jQuery( document ).on( 'show.bs.dropdown', '#frm_field_group_controls', onFieldGroupActionDropdownShow );
10590 $builderForm.on( 'click', '.frm_single_option a[data-removeid]', deleteFieldOption );
10591 $builderForm.on( 'mousedown', '.frm_single_option input[type=radio]', maybeUncheckRadio );
10592 $builderForm.on( 'focusin', '.frm_single_option input[type=text]', maybeClearOptText );
10593 $builderForm.on( 'click', '.frm_add_opt', addFieldOption );
10594 $builderForm.on( 'change', '.frm_single_option input', resetOptOnChange );
10595 $builderForm.on( 'change', '.frm_image_id', resetOptOnChange );
10596 $builderForm.on( 'change', '.frm_toggle_mult_sel', toggleMultSel );
10597 $builderForm.on( 'focusin', '.frm_classes', showBuilderModal );
10598
10599 $newFields.on( 'click', '.frm_primary_label', clickLabel );
10600 $newFields.on( 'click', '.frm_description', clickDescription );
10601 $newFields.on( 'click', 'li.ui-state-default:not(.frm_noallow)', clickVis );
10602 $newFields.on( 'dblclick', 'li.ui-state-default', openAdvanced );
10603 $builderForm.on( 'change', '.frm_tax_form_select', toggleFormTax );
10604 $builderForm.on( 'change', 'select.conf_field', addConf );
10605
10606 $builderForm.on( 'change', '.frm_get_field_selection', getFieldSelection );
10607
10608 $builderForm.on( 'click', '.frm-show-inline-modal', maybeShowInlineModal );
10609
10610 $builderForm.on( 'click', '.frm-inline-modal .dismiss', dismissInlineModal );
10611 jQuery( document ).on( 'change', '[data-frmchange]', changeInputtedValue );
10612
10613 $builderForm.on( 'change', '.frm_include_extras_field', rePopCalcFieldsForSummary );
10614 $builderForm.on( 'change', 'select[name^="field_options[form_select_"]', maybeChangeEmbedFormMsg );
10615
10616 jQuery( document ).on( 'submit', '#frm_js_build_form', buildSubmittedNoAjax );
10617 jQuery( document ).on( 'change', '#frm_builder_page input:not(.frm-search-input):not(.frm-custom-grid-size-input), #frm_builder_page select, #frm_builder_page textarea', fieldUpdated );
10618
10619 popAllProductFields();
10620
10621 jQuery( document ).on( 'change', '.frmjs_prod_data_type_opt', toggleProductType );
10622
10623 jQuery( document ).on( 'focus', '.frm-single-settings ul input[type="text"][name^="field_options[options_"]', onOptionTextFocus );
10624 jQuery( document ).on( 'blur', '.frm-single-settings ul input[type="text"][name^="field_options[options_"]', onOptionTextBlur );
10625
10626 frmDom.util.documentOn( 'click', '.frm-show-field-settings', clickVis );
10627 frmDom.util.documentOn( 'change', 'select.frm_format_dropdown, select.frm_phone_type_dropdown', maybeUpdateFormatInput );
10628
10629 // Navigate to the next input field on pressing Enter in a single option field
10630 $builderForm.on( 'keydown', '.frm_single_option input[name^="field_options["], .frm_single_option input[name^="rows_"]', event => {
10631 if ( 'Enter' === event.key ) {
10632 focusNextSingleOptionInput( event.currentTarget );
10633 }
10634 });
10635
10636 initBulkOptionsOverlay();
10637 hideEmptyEle();
10638 maybeHideQuantityProductFieldOption();
10639 handleNameFieldOnFormBuilder();
10640 toggleSectionHolder();
10641 handleShowPasswordLiveUpdate();
10642 document.addEventListener( 'scroll', updateShortcodesPopupPosition, true );
10643 document.addEventListener( 'change', handleBuilderChangeEvent );
10644 document.querySelector( '.frm_form_builder' ).addEventListener( 'mousedown', event => {
10645 if ( event.shiftKey ) {
10646 event.preventDefault();
10647 }
10648 });
10649 },
10650
10651 settingsInit: function() {
10652 const $formActions = jQuery( document.getElementById( 'frm_notification_settings' ) );
10653
10654 let formSettings, $loggedIn, $cookieExp, $editable;
10655
10656 // BCC, CC, and Reply To button functionality
10657 $formActions.on( 'click', '.frm_email_buttons', showEmailRow );
10658 $formActions.on( 'click', '.frm_remove_field', hideEmailRow );
10659 $formActions.on( 'change', '.frm_to_row, .frm_from_row', showEmailWarning );
10660 $formActions.on( 'change', '.frm_tax_selector', changePosttaxRow );
10661 $formActions.on( 'change', 'select.frm_single_post_field', checkDupPost );
10662 $formActions.on( 'change', 'select.frm_toggle_post_content', togglePostContent );
10663 $formActions.on( 'change', 'select.frm_dyncontent_opt', fillDyncontent );
10664 $formActions.on( 'change', '.frm_post_type', switchPostType );
10665 $formActions.on( 'click', '.frm_add_postmeta_row', addPostmetaRow );
10666 $formActions.on( 'click', '.frm_add_posttax_row', addPosttaxRow );
10667 $formActions.on( 'click', '.frm_toggle_cf_opts', toggleCfOpts );
10668 $formActions.on( 'click', '.frm_duplicate_form_action', copyFormAction );
10669 jQuery( '.frm_actions_list' ).on( 'click', '.frm_active_action', addFormAction );
10670 jQuery( '#frm-show-groups, #frm-hide-groups' ).on( 'click', toggleActionGroups );
10671 initiateMultiselect();
10672
10673 //set actions icons to inactive
10674 jQuery( 'ul.frm_actions_list li' ).each( function() {
10675 checkActiveAction( jQuery( this ).children( 'a' ).data( 'actiontype' ) );
10676
10677 // If the icon is a background image, don't add BG color.
10678 const icon = jQuery( this ).find( 'i' );
10679 if ( icon.css( 'background-image' ) !== 'none' ) {
10680 icon.addClass( 'frm-inverse' );
10681 }
10682 });
10683
10684 jQuery( '.frm_submit_settings_btn' ).on( 'click', submitSettings );
10685
10686 addFormNameModalEvents();
10687
10688 formSettings = jQuery( '.frm_form_settings' );
10689 formSettings.on( 'click', '.frm_add_form_logic', addFormLogicRow );
10690 formSettings.on( 'click', '.frm_already_used', actionLimitMessage );
10691
10692 document.addEventListener(
10693 'click',
10694 function handleImageUploadClickEvents( event ) {
10695 const { target } = event;
10696
10697 if ( ! target.closest( '.frm_image_preview_wrapper' ) ) {
10698 return;
10699 }
10700
10701 if ( target.closest( '.frm_choose_image_box' ) ) {
10702 addImageToOption.bind( target )( event );
10703 return;
10704 }
10705
10706 if ( target.closest( '.frm_remove_image_option' ) ) {
10707 removeImageFromOption.bind( target )( event );
10708 }
10709 }
10710 );
10711
10712 // Close shortcode modal on click.
10713 formSettings.on( 'mouseup', '*:not(.frm-show-box)', maybeHideShortcodes );
10714
10715 //Warning when user selects "Do not store entries ..."
10716 jQuery( document.getElementById( 'no_save' ) ).on( 'change', function() {
10717 if ( this.checked ) {
10718 if ( confirm( frmAdminJs.no_save_warning ) !== true ) {
10719 // Uncheck box if user hits "Cancel"
10720 jQuery( this ).attr( 'checked', false );
10721 }
10722 }
10723 });
10724
10725 jQuery( 'select[name="options[edit_action]"]' ).on( 'change', showSuccessOpt );
10726
10727 $loggedIn = document.getElementById( 'logged_in' );
10728 jQuery( $loggedIn ).on( 'change', function() {
10729 if ( this.checked ) {
10730 visible( '.hide_logged_in' );
10731 } else {
10732 invisible( '.hide_logged_in' );
10733 }
10734 });
10735
10736 $cookieExp = jQuery( document.getElementById( 'frm_cookie_expiration' ) );
10737 jQuery( document.getElementById( 'frm_single_entry_type' ) ).on( 'change', function() {
10738 if ( this.value === 'cookie' ) {
10739 $cookieExp.fadeIn( 'slow' );
10740 } else {
10741 $cookieExp.fadeOut( 'slow' );
10742 }
10743 });
10744
10745 const $singleEntry = document.getElementById( 'single_entry' );
10746 jQuery( $singleEntry ).on( 'change', function() {
10747 if ( this.checked ) {
10748 visible( '.hide_single_entry' );
10749 } else {
10750 invisible( '.hide_single_entry' );
10751 }
10752
10753 if ( this.checked && jQuery( document.getElementById( 'frm_single_entry_type' ) ).val() === 'cookie' ) {
10754 $cookieExp.fadeIn( 'slow' );
10755 } else {
10756 $cookieExp.fadeOut( 'slow' );
10757 }
10758 });
10759
10760 jQuery( '.hide_save_draft' ).hide();
10761
10762 const $saveDraft = jQuery( document.getElementById( 'save_draft' ) );
10763 $saveDraft.on( 'change', function() {
10764 if ( this.checked ) {
10765 jQuery( '.hide_save_draft' ).fadeIn( 'slow' );
10766 } else {
10767 jQuery( '.hide_save_draft' ).fadeOut( 'slow' );
10768 }
10769 });
10770 triggerChange( $saveDraft );
10771
10772 //If Allow editing is checked/unchecked
10773 $editable = document.getElementById( 'editable' );
10774 jQuery( $editable ).on( 'change', function() {
10775 if ( this.checked ) {
10776 jQuery( '.hide_editable' ).fadeIn( 'slow' );
10777 triggerChange( document.getElementById( 'edit_action' ) );
10778 } else {
10779 jQuery( '.hide_editable' ).fadeOut( 'slow' );
10780 jQuery( '.edit_action_message_box' ).fadeOut( 'slow' );//Hide On Update message box
10781 }
10782 });
10783
10784 //If File Protection is checked/unchecked
10785 jQuery( document ).on( 'change', '#protect_files', function() {
10786 if ( this.checked ) {
10787 jQuery( '.hide_protect_files' ).fadeIn( 'slow' );
10788 } else {
10789 jQuery( '.hide_protect_files' ).fadeOut( 'slow' );
10790 }
10791 });
10792
10793 jQuery( document ).on( 'frm-multiselect-changed', '#protect_files_role', adjustVisibilityValuesForEveryoneValues );
10794
10795 jQuery( document ).on( 'submit', '.frm_form_settings', settingsSubmitted );
10796 jQuery( document ).on( 'change', '#form_settings_page input:not(.frm-search-input), #form_settings_page select, #form_settings_page textarea', fieldUpdated );
10797
10798 // Page Selection Autocomplete
10799 initAutocomplete();
10800
10801 jQuery( document ).on( 'frm-action-loaded', onActionLoaded );
10802
10803 initOnSubmitAction();
10804 },
10805
10806 panelInit: function() {
10807 let customPanel, settingsPage, viewPage, insertFieldsTab;
10808
10809 jQuery( '.frm_wrap, #postbox-container-1' ).on( 'click', '.frm_insert_code', insertCode );
10810 jQuery( document ).on( 'change', '.frm_insert_val', function() {
10811 insertFieldCode( jQuery( this ).data( 'target' ), jQuery( this ).val() );
10812 jQuery( this ).val( '' );
10813 });
10814
10815 jQuery( document ).on( 'click change', '#frm-id-key-condition', resetLogicBuilder );
10816 jQuery( document ).on( 'keyup change', '.frm-build-logic', setLogicExample );
10817
10818 showInputIcon();
10819 jQuery( document ).on( 'frmElementAdded', function( event, parentEle ) {
10820 /* This is here for add-ons to trigger */
10821 showInputIcon( parentEle );
10822 });
10823 jQuery( document ).on( 'mousedown', '.frm-show-box', showShortcodes );
10824
10825 settingsPage = document.getElementById( 'form_settings_page' );
10826 viewPage = document.body.classList.contains( 'post-type-frm_display' );
10827 insertFieldsTab = document.getElementById( 'frm_insert_fields_tab' );
10828
10829 if ( settingsPage !== null || viewPage || builderPage ) {
10830 jQuery( document ).on( 'focusin', 'form input, form textarea', function( e ) {
10831 let htmlTab;
10832 e.stopPropagation();
10833 maybeShowModal( this );
10834
10835 if ( jQuery( this ).is( ':not(:submit, input[type=button], .frm-search-input, input[type=checkbox])' ) ) {
10836 if ( jQuery( e.target ).closest( '#frm_adv_info' ).length ) {
10837 // Don't trigger for fields inside of the modal.
10838 return;
10839 }
10840
10841 if ( settingsPage !== null || builderPage ) {
10842 /* form settings page */
10843 htmlTab = jQuery( '#frm_html_tab' );
10844 if ( jQuery( this ).closest( '#html_settings' ).length > 0 ) {
10845 htmlTab.show();
10846 htmlTab.siblings().hide();
10847 jQuery( '#frm_html_tab a' ).trigger( 'click' );
10848 toggleAllowedHTML( this );
10849 } else {
10850 showElement( jQuery( '.frm-category-tabs li' ) );
10851 insertFieldsTab.click();
10852 htmlTab.hide();
10853 htmlTab.siblings().show();
10854 }
10855 } else if ( viewPage ) {
10856 const event = new CustomEvent( 'frm_legacy_views_handle_field_focus' );
10857 event.frmData = { idAttrValue: this.id };
10858 document.dispatchEvent( event );
10859 }
10860 }
10861 });
10862 }
10863
10864 jQuery( '.frm_wrap, #postbox-container-1' ).on( 'mousedown', '#frm_adv_info a, .frm_field_list a', function( e ) {
10865 e.preventDefault();
10866 });
10867
10868 customPanel = jQuery( '#frm_adv_info' );
10869 customPanel.on( 'click', '.subsubsub a.frmids', function( e ) {
10870 toggleKeyID( 'frmids', e );
10871 });
10872 customPanel.on( 'click', '.subsubsub a.frmkeys', function( e ) {
10873 toggleKeyID( 'frmkeys', e );
10874 });
10875 },
10876
10877 inboxInit: function() {
10878 jQuery( '.frm_inbox_dismiss' ).on( 'click', function( e ) {
10879 const message = this.parentNode.parentNode;
10880 const key = message.getAttribute( 'data-message' );
10881 const href = this.getAttribute( 'href' );
10882 const dismissedMessage = message.cloneNode( true );
10883 const dismissedMessagesWrapper = document.querySelector( '.frm-dismissed-inbox-messages' );
10884
10885 if ( 'free_templates' === key && ! this.classList.contains( 'frm_inbox_dismiss' ) ) {
10886 return;
10887 }
10888
10889 e.preventDefault();
10890
10891 data = {
10892 action: 'frm_inbox_dismiss',
10893 key,
10894 nonce: frmGlobal.nonce
10895 };
10896
10897 const isInboxSlideIn = 'frm_inbox_slide_in' === message.id;
10898 if ( isInboxSlideIn ) {
10899 message.classList.remove( 's11-fadein' );
10900 message.classList.add( 's11-fadeout' );
10901 message.addEventListener( 'animationend', () => message.remove(), { once: true });
10902 }
10903
10904 postAjax(
10905 data,
10906 () => {
10907 if ( isInboxSlideIn ) {
10908 return;
10909 }
10910
10911 if ( href !== '#' ) {
10912 window.location = href;
10913 return true;
10914 }
10915
10916 fadeOut(
10917 message,
10918 () => {
10919 if ( null !== dismissedMessagesWrapper ) {
10920 dismissedMessage.classList.remove( 'frm-fade' );
10921 dismissedMessage.querySelector( '.frm-inbox-message-heading' )?.removeChild( dismissedMessage.querySelector( '.frm-inbox-message-heading .frm_inbox_dismiss' ) );
10922 dismissedMessagesWrapper.append( dismissedMessage );
10923 }
10924 if ( 1 === message.parentNode.querySelectorAll( '.frm-inbox-message-container' ).length ) {
10925 document.getElementById( 'frm_empty_inbox' ).classList.remove( 'frm_hidden' );
10926 message.parentNode.closest( '.frm-active' ).classList.add( 'frm-empty-inbox' );
10927 showActiveCampaignForm();
10928 }
10929 message.parentNode.removeChild( message );
10930 }
10931 );
10932 }
10933 );
10934 });
10935
10936 if ( false === document.getElementById( 'frm_empty_inbox' )?.classList.contains( 'frm_hidden' ) ) {
10937 showActiveCampaignForm();
10938 }
10939 },
10940
10941 solutionInit: function() {
10942 jQuery( document ).on( 'submit', '#frm-new-template', installTemplate );
10943 },
10944
10945 styleInit: function() {
10946 const $previewWrapper = jQuery( '.frm_image_preview_wrapper' );
10947 $previewWrapper.on( 'click', '.frm_choose_image_box', addImageToOption );
10948 $previewWrapper.on( 'click', '.frm_remove_image_option', removeImageFromOption );
10949
10950 wp.hooks.doAction( 'frm_style_editor_init' );
10951 },
10952
10953 customCSSInit: function() {
10954 console.warn( 'Calling frmAdminBuild.customCSSInit is deprecated.' );
10955 },
10956
10957 globalSettingsInit: function() {
10958 let licenseTab;
10959
10960 jQuery( document ).on( 'click', '[data-frmuninstall]', uninstallNow );
10961
10962 initiateMultiselect();
10963
10964 // activate addon licenses
10965 licenseTab = document.getElementById( 'licenses_settings' );
10966 if ( licenseTab !== null ) {
10967 jQuery( licenseTab ).on( 'click', '.edd_frm_save_license', saveAddonLicense );
10968 }
10969
10970 // Solution install page
10971 jQuery( document ).on( 'click', '#frm-new-template button', installTemplateFieldset );
10972
10973 jQuery( '#frm-dismissable-cta .dismiss' ).on( 'click', function( event ) {
10974 event.preventDefault();
10975 jQuery.post(
10976 ajaxurl,
10977 {
10978 action: 'frm_lite_settings_upgrade',
10979 nonce: frmGlobal.nonce
10980 }
10981 );
10982 jQuery( '.settings-lite-cta' ).remove();
10983 });
10984
10985 const captchaType = document.getElementById( 'frm_re_type' );
10986 if ( captchaType ) {
10987 captchaType.addEventListener( 'change', handleCaptchaTypeChange );
10988 }
10989
10990 document.querySelector( '.frm_captchas' ).addEventListener( 'change', function( event ) {
10991 const captchaValueOnLoad = document.querySelector( '.frm_captchas input[checked="checked"]' )?.value;
10992 const showNote = event.target.value !== captchaValueOnLoad;
10993 document.querySelector( '.captcha_settings .frm_note_style' ).classList.toggle( 'frm_hidden', ! showNote );
10994 });
10995
10996 // Set fieldsUpdated to 0 to avoid the unsaved changes pop up.
10997 frmDom.util.documentOn( 'submit', '.frm_settings_form', () => fieldsUpdated = 0 );
10998
10999 const manageStyleSettings = document.getElementById( 'manage_styles_settings' );
11000 if ( manageStyleSettings ) {
11001 manageStyleSettings.addEventListener(
11002 'change',
11003 event => {
11004 const target = event.target;
11005 if ( 'SELECT' !== target.nodeName || ! target.dataset.name || target.getAttribute( 'name' ) ) {
11006 return;
11007 }
11008
11009 target.setAttribute( 'name', target.dataset.name );
11010 }
11011 );
11012 }
11013
11014 const paymentsSettings = document.getElementById( 'payments_settings' );
11015 const paymentSettingsTabs = paymentsSettings?.querySelectorAll( '[name="frm_payment_section"]' );
11016 if ( paymentSettingsTabs ) {
11017 paymentSettingsTabs.forEach(
11018 element => {
11019 element.addEventListener( 'change', () => {
11020 if ( ! element.checked ) {
11021 return;
11022 }
11023
11024 const label = paymentsSettings.querySelector( `label[for="${ element.id }"]` );
11025 if ( label ) {
11026 label.setAttribute( 'aria-selected', 'true' );
11027 }
11028
11029 paymentSettingsTabs.forEach(
11030 tab => {
11031 if ( tab === element ) {
11032 return;
11033 }
11034
11035 const label = paymentsSettings.querySelector( `label[for="${ tab.id }"]` );
11036 if ( label ) {
11037 label.setAttribute( 'aria-selected', 'false' );
11038 }
11039 }
11040 );
11041 });
11042 }
11043 );
11044 }
11045 },
11046
11047 exportInit: function() {
11048 jQuery( '.frm_form_importer' ).on( 'submit', startFormMigration );
11049 jQuery( document.getElementById( 'frm_export_xml' ) ).on( 'submit', validateExport );
11050 jQuery( '#frm_export_xml input, #frm_export_xml select' ).on( 'change', removeExportError );
11051 jQuery( 'input[name="frm_import_file"]' ).on( 'change', checkCSVExtension );
11052 document.querySelector( 'select[name="format"]' ).addEventListener( 'change', exportTypeChanged );
11053
11054 jQuery( 'input[name="frm_export_forms[]"]' ).on( 'click', preventMultipleExport );
11055 initiateMultiselect();
11056
11057 jQuery( '.frm-feature-banner .dismiss' ).on( 'click', function( event ) {
11058 event.preventDefault();
11059 jQuery.post( ajaxurl, {
11060 action: 'frm_dismiss_migrator',
11061 plugin: this.id,
11062 nonce: frmGlobal.nonce
11063 });
11064 this.parentElement.remove();
11065 });
11066
11067 showOrHideRepeaters( getExportOption() );
11068
11069 document.querySelector( '#frm-export-select-all' ).addEventListener( 'change', event => {
11070 document.querySelectorAll( '[name="frm_export_forms[]"]' ).forEach( cb => cb.checked = event.target.checked );
11071 });
11072 },
11073
11074 inboxBannerInit: function() {
11075 const banner = document.getElementById( 'frm_banner' );
11076 if ( ! banner ) {
11077 return;
11078 }
11079
11080 const dismissButton = banner.querySelector( '.frm-banner-dismiss' );
11081 document.addEventListener(
11082 'click',
11083 function( event ) {
11084 if ( event.target !== dismissButton ) {
11085 return;
11086 }
11087
11088 const data = {
11089 action: 'frm_inbox_dismiss',
11090 key: banner.dataset.key,
11091 nonce: frmGlobal.nonce
11092 };
11093 postAjax(
11094 data,
11095 function() {
11096 jQuery( banner ).fadeOut(
11097 400,
11098 function() {
11099 banner.remove();
11100 }
11101 );
11102 }
11103 );
11104 }
11105 );
11106 },
11107
11108 updateOpts: function( fieldId, opts, modal ) {
11109 const separate = usingSeparateValues( fieldId ),
11110 action = isProductField( fieldId ) ? 'frm_bulk_products' : 'frm_import_options';
11111 jQuery.ajax({
11112 type: 'POST',
11113 url: ajaxurl,
11114 data: {
11115 action: action,
11116 field_id: fieldId,
11117 opts: opts,
11118 separate: separate,
11119 nonce: frmGlobal.nonce
11120 },
11121 success: function( html ) {
11122 document.getElementById( 'frm_field_' + fieldId + '_opts' ).innerHTML = html;
11123 wp.hooks.doAction( 'frm_after_bulk_edit_opts', fieldId );
11124 resetDisplayedOpts( fieldId );
11125
11126 if ( typeof modal !== 'undefined' ) {
11127 modal.dialog( 'close' );
11128 document.getElementById( 'frm-update-bulk-opts' ).classList.remove( 'frm_loading_button' );
11129 }
11130 }
11131 });
11132 },
11133
11134 /* remove conditional logic if the field doesn't exist */
11135 triggerRemoveLogic: function( fieldID, metaName ) {
11136 jQuery( '#frm_logic_' + fieldID + '_' + metaName + ' .frm_remove_tag' ).trigger( 'click' );
11137 },
11138
11139 downloadXML: function( controller, ids, isTemplate ) {
11140 let url = ajaxurl + '?action=frm_' + controller + '_xml&ids=' + ids;
11141 if ( isTemplate !== null ) {
11142 url = url + '&is_template=' + isTemplate;
11143 }
11144 location.href = url;
11145 },
11146
11147 /**
11148 * @since 5.0.04
11149 */
11150 hooks: {
11151 applyFilters: function( hookName, ...args ) {
11152 return wp.hooks.applyFilters( hookName, ...args );
11153 },
11154 addFilter: function( hookName, callback, priority ) {
11155 return wp.hooks.addFilter( hookName, 'formidable', callback, priority );
11156 },
11157 doAction: function( hookName, ...args ) {
11158 return wp.hooks.doAction( hookName, ...args );
11159 },
11160 addAction: function( hookName, callback, priority ) {
11161 return wp.hooks.addAction( hookName, 'formidable', callback, priority );
11162 }
11163 },
11164
11165 applyZebraStriping,
11166 initModal,
11167 infoModal,
11168 offsetModalY,
11169 adjustConditionalLogicOptionOrders,
11170 addRadioCheckboxOpt,
11171 installNewForm,
11172 toggleAddonState,
11173 purifyHtml,
11174 loadApiEmailForm,
11175 addMyEmailAddress,
11176 fillDropdownOpts,
11177 showSaveAndReloadModal,
11178 deleteField,
11179 insertFormField,
11180 confirmLinkClick,
11181 handleInsertFieldByDraggingResponse,
11182 handleAddFieldClickResponse,
11183 syncLayoutClasses,
11184 };
11185 }
11186
11187 window.frmAdminBuild = frmAdminBuildJS();
11188
11189 jQuery( document ).ready(
11190 () => {
11191 frmAdminBuild.init();
11192
11193 frmDom.bootstrap.setupBootstrapDropdowns( convertOldBootstrapDropdownsToBootstrap4 );
11194 document.querySelector( '.preview.dropdown .frm-dropdown-toggle' )?.setAttribute( 'data-toggle', 'dropdown' );
11195
11196 function convertOldBootstrapDropdownsToBootstrap4( frmDropdownMenu ) {
11197 const toggle = frmDropdownMenu.querySelector( '.frm-dropdown-toggle' );
11198 if ( toggle ) {
11199 if ( ! toggle.hasAttribute( 'role' ) ) {
11200 toggle.setAttribute( 'role', 'button' );
11201 }
11202 if ( ! toggle.hasAttribute( 'tabindex' ) ) {
11203 toggle.setAttribute( 'tabindex', 0 );
11204 }
11205 }
11206
11207 // Convert <li> and <ul> tags.
11208 if ( 'UL' === frmDropdownMenu.tagName ) {
11209 convertBootstrapUl( frmDropdownMenu );
11210 }
11211 }
11212
11213 function convertBootstrapUl( ul ) {
11214 let html = ul.outerHTML;
11215 html = html.replace( '<ul ', '<div ' );
11216 html = html.replace( '</ul>', '</div>' );
11217 html = html.replaceAll( '<li>', '<div class="dropdown-item">' );
11218 html = html.replaceAll( '<li class="', '<div class="dropdown-item ' );
11219 html = html.replaceAll( '</li>', '</div>' );
11220 ul.outerHTML = html;
11221 }
11222 }
11223 );
11224
11225 function frm_show_div( div, value, showIf, classId ) { // eslint-disable-line camelcase
11226 if ( value == showIf ) {
11227 jQuery( classId + div ).fadeIn( 'slow' ).css( 'visibility', 'visible' );
11228 } else {
11229 jQuery( classId + div ).fadeOut( 'slow' );
11230 }
11231 }
11232
11233 function frmCheckAll( checked, n ) {
11234 jQuery( 'input[name^="' + n + '"]' ).prop( 'checked', ! ! checked );
11235 }
11236
11237 function frmCheckAllLevel( checked, n, level ) {
11238 const $kids = jQuery( '.frm_catlevel_' + level ).children( '.frm_checkbox' ).children( 'label' );
11239 $kids.children( 'input[name^="' + n + '"]' ).prop( 'checked', ! ! checked );
11240 }
11241
11242 function frmGetFieldValues( fieldId, cur, rowNumber, fieldType, htmlName, callback ) {
11243 if ( ! fieldId ) {
11244 return;
11245 }
11246
11247 jQuery.ajax({
11248 type: 'POST', url: ajaxurl,
11249 data: 'action=frm_get_field_values&current_field=' + cur + '&field_id=' + fieldId + '&name=' + htmlName + '&t=' + fieldType + '&form_action=' + jQuery( 'input[name="frm_action"]' ).val() + '&nonce=' + frmGlobal.nonce,
11250 success: function( msg ) {
11251 document.getElementById( 'frm_show_selected_values_' + cur + '_' + rowNumber ).innerHTML = msg;
11252
11253 if ( 'function' === typeof callback ) {
11254 callback();
11255 }
11256 }
11257 });
11258 }
11259
11260 function frmImportCsv( formID ) {
11261 let urlVars = '';
11262 if ( typeof __FRMURLVARS !== 'undefined' ) {
11263 urlVars = __FRMURLVARS;
11264 }
11265
11266 jQuery.ajax({
11267 type: 'POST', url: ajaxurl,
11268 data: 'action=frm_import_csv&nonce=' + frmGlobal.nonce + '&frm_skip_cookie=1' + urlVars,
11269 success: function( count ) {
11270 const max = jQuery( '.frm_admin_progress_bar' ).attr( 'aria-valuemax' );
11271 const imported = max - count;
11272 const percent = ( imported / max ) * 100;
11273 jQuery( '.frm_admin_progress_bar' ).css( 'width', percent + '%' ).attr( 'aria-valuenow', imported );
11274
11275 if ( parseInt( count, 10 ) > 0 ) {
11276 jQuery( '.frm_csv_remaining' ).html( count );
11277 frmImportCsv( formID );
11278 } else {
11279 jQuery( document.getElementById( 'frm_import_message' ) ).html( frm_admin_js.import_complete ); // eslint-disable-line camelcase
11280 setTimeout( function() {
11281 location.href = '?page=formidable-entries&frm_action=list&form=' + formID + '&import-message=1';
11282 }, 2000 );
11283 }
11284 }
11285 });
11286 }
11287