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

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