PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.22.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.22.1
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.22.1, at js/formidable_admin.js

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