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

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