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

style.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.24.1, at js/admin/style.js

1,495 lines 45.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * This script file handles style settings in the Lite plugin.
3 * Pro-specific features are in the style-settings.js file in Pro.
4 */
5 ( function() {
6 /* globals frmDom, frmAdminBuild */
7 'use strict';
8
9 if ( ! document.getElementById( 'frm_active_style_form' ) ) {
10 return;
11 }
12
13 const { __, sprintf } = wp.i18n;
14 const state = {
15 showingSampleForm: document.getElementById( 'frm_active_style_form' ).classList.contains( 'frm_hidden' ), // boolean
16 unsavedChanges: false, // boolean
17 autoId: 0, // Number
18 // Track the value of the selected style ID on page (on the list page).
19 // This is tracked to determine if there are actually unsaved changes.
20 // This way when you switch back to the initial value it doesn't count as a change.
21 initialSelectedStyleValue: false // String|false
22 };
23 const { div, span, a, labelledTextInput, tag, svg, success } = frmDom;
24 const { onClickPreventDefault } = frmDom.util;
25 const { maybeCreateModal, footerButton } = frmDom.modal;
26 const { doJsonPost } = frmDom.ajax;
27
28 const isListPage = document.getElementsByClassName( 'frm-style-card' ).length > 0;
29 if ( isListPage ) {
30 initListPage();
31 }
32
33 initCommonEventListeners();
34 initPreview();
35 fixWpAuthModal();
36
37 /**
38 * These are shared events for both the edit/list views like the sample form toggle.
39 * This includes preview events, but also the update button click event handling for both views.
40 *
41 * @return {void}
42 */
43 function initCommonEventListeners() {
44 document.addEventListener( 'click', handleCommonClickEvents );
45 window.addEventListener( 'beforeunload', maybeConfirmExit );
46 disablePreviewSubmitButtons();
47 }
48
49 /**
50 * Initialize common functions required for the preview in both the edit and list views.
51 *
52 * @return {void}
53 */
54 function initPreview() {
55 initFloatingLabels();
56 fillMissingSignatureValidationFunction();
57 setSelectPlaceholderColor();
58
59 // Remove .wp-core-ui from the body so the preview can avoid it.
60 // Then add it back where we want to use admin styles (the sidebar, otherwise inputs appear short).
61 document.body.classList.remove( 'wp-core-ui' );
62 document.getElementById( 'frm_style_sidebar' ).classList.add( 'wp-core-ui' );
63
64 jQuery( document ).on( 'input change', 'input[data-frmrange]', initSliderPreview );
65 }
66
67 /**
68 * Initialize the slider functionality in the style preview.
69 *
70 * @param {HTMLElement} event
71 * @return {void}
72 */
73 function initSliderPreview( event ) {
74 const wrapper = event.target.closest( '.frm_range_container' );
75 if ( null === wrapper ) {
76 return;
77 }
78 wrapper.querySelector( '.frm_range_value' ).innerHTML = parseInt( this.value, 10 );
79 }
80
81 /**
82 * Add the wp-core-ui class to the #wp-auth-check-wrap element.
83 * As this style isn't included on the body for the styler, the close button on the auth modal wasn't getting styled properly.
84 *
85 * @return {void}
86 */
87 function fixWpAuthModal() {
88 const authWrap = document.getElementById( 'wp-auth-check-wrap' );
89 if ( authWrap ) {
90 authWrap.classList.add( 'wp-core-ui' );
91 }
92 }
93
94 /**
95 * @return {void}
96 */
97 function initListPage() {
98 document.addEventListener( 'click', handleClickEventsForListPage );
99 // Add a timeout so Pro has a chance to add a filter first.
100 // 0 does not always work in Google Chrome, so use 1.
101 setTimeout( addHamburgerMenusToCards, 1 );
102 initDatepickerSample();
103
104 const enableToggle = document.getElementById( 'frm_enable_styling' );
105 const styleIdInput = getStyleIdInput();
106 state.initialSelectedStyleValue = styleIdInput.value;
107
108 enableToggle.addEventListener( 'change', handleEnableStylingToggleChange );
109
110 syncPreviewFormLabelPositionsWithActiveStyle();
111 initStyleCardPagination();
112 }
113
114 /**
115 * Update label position in preview on list page.
116 * On the edit page this is handled with the initPosClass function instead.
117 */
118 function syncPreviewFormLabelPositionsWithActiveStyle() {
119 const activeCard = getActiveCard();
120 if ( activeCard ) {
121 changeLabelPositionsInPreview( activeCard.dataset.labelPosition );
122 }
123 }
124
125 /**
126 * Handle pagination click events.
127 *
128 * @return {void}
129 */
130 function initStyleCardPagination() {
131 document.querySelectorAll( '.frm-style-card-pagination' ).forEach(
132 pagination => {
133 const wrapper = pagination.closest( '.frm-style-card-wrapper' );
134 const showAllAnchor = pagination.querySelector( '.frm-show-all-styles' );
135 let showingAll = false;
136
137 onClickPreventDefault(
138 showAllAnchor,
139 () => {
140 showingAll = ! showingAll;
141
142 if ( showingAll ) {
143 wrapper.querySelectorAll( '.frm-style-card' ).forEach(
144 card => card.classList.remove( 'frm_hidden' )
145 );
146 showAllAnchor.textContent = __( 'Show less', 'formidable' );
147 return;
148 }
149
150 wrapper.querySelectorAll( '.frm-style-card:nth-child(3) ~ .frm-style-card' ).forEach(
151 card => card.classList.add( 'frm_hidden' )
152 );
153 const hiddenCount = wrapper.querySelectorAll( '.frm-style-card.frm_hidden' ).length;
154 /* translators: %d: The number of hidden items to show. */
155 showAllAnchor.textContent = sprintf( __( 'Show all (%d)', 'formidable' ), hiddenCount );
156 }
157 );
158 }
159 );
160 }
161
162 /**
163 * @param {string} labelPosition
164 * @return {void}
165 */
166 function changeLabelPositionsInPreview( labelPosition ) {
167 const input = tag( 'input' );
168 input.value = labelPosition;
169 setPosClass.bind( input )();
170 }
171
172 /**
173 * @return {HTMLElement} The active style card.
174 */
175 function getActiveCard() {
176 return document.querySelector( '.frm-active-style-card' );
177 }
178
179 /**
180 * When Formidable styling is disabled, the list of styles fades out.
181 * The style ID value associated with the selected style card gets cleared.
182 * This is because disabling styles is linked to the custom_style option as well.
183 *
184 * @param {Event} event
185 * @return {void}
186 */
187 function handleEnableStylingToggleChange( event ) {
188 const stylesEnabled = event.target.checked;
189
190 document.querySelectorAll( '.frm-style-card-wrapper' ).forEach(
191 cardWrapper => cardWrapper.classList.toggle( 'frm-styles-enabled', stylesEnabled )
192 );
193
194 if ( ! stylesEnabled ) {
195 const styleIdInput = getStyleIdInput();
196 styleIdInput.value = '0';
197 trackListPageChange();
198 toggleFormidableStylingInPreviewForms( false );
199 return;
200 }
201
202 toggleFormidableStylingInPreviewForms( true );
203
204 // Click the active card so the style id input properly syncs.
205 // In Pro, templates use a templateKey attribute so we don't always want card.dataset.styleId
206 // There is no need to call trackListPageChange as it happens in the click event.
207 const card = document.querySelector( '.frm-active-style-card' );
208 if ( card ) {
209 card.click();
210 }
211 }
212
213 /**
214 * Track unsaved changes on the list page.
215 * All settings on the list page are mapped to the value of the styleIdInput.
216 * We track the value on load with state.initialSelectedStyleValue.
217 * Only consider unsaved changes on the page when this variable is no longer set to the original value.
218 *
219 * @return {void}
220 */
221 function trackListPageChange() {
222 const styleIdInput = getStyleIdInput();
223 state.unsavedChanges = styleIdInput.value !== state.initialSelectedStyleValue;
224 }
225
226 /**
227 * @param {boolean} on
228 * @return {void}
229 */
230 function toggleFormidableStylingInPreviewForms( on ) {
231 const preview = document.getElementById( 'frm_style_preview' );
232 const activeCard = getActiveCard();
233
234 let selector = '.frm_forms';
235 if ( ! on ) {
236 selector += '.with_frm_style';
237 }
238
239 preview.querySelectorAll( selector ).forEach(
240 formParent => {
241 formParent.classList.toggle( 'with_frm_style', on );
242 formParent.classList.toggle( activeCard.dataset.classname, on );
243 }
244 );
245 }
246
247 /**
248 * @return {HTMLElement} The style ID input element.
249 */
250 function getStyleIdInput() {
251 return document.getElementById( 'frm_style_list_form' ).querySelector( '[name="style_id"]' );
252 }
253
254 /**
255 * @param {Event} event
256 * @return {void}
257 */
258 function handleCommonClickEvents( event ) {
259 const target = event.target;
260
261 if ( 'frm_toggle_sample_form' === target.id || target.closest( '#frm_toggle_sample_form' ) ) {
262 toggleSampleForm();
263 return;
264 }
265
266 if ( 'frm_submit_side_top' === target.id || target.closest( '#frm_submit_side_top' ) || 'frm-style-advanced-settings-button' === target.id || target.closest( 'a#frm_style_back_to_quick_settings' ) ) {
267 switchAdvancedSettingsFormAction( target );
268 handleUpdateClick();
269 return;
270 }
271
272 if ( target.classList.contains( 'frm-edit-style' ) || null !== target.closest( '.frm-edit-style' ) || 'frm_edit_style' === target.id ) {
273 modifyStylerUrl( target );
274 return; // eslint-disable-line
275 }
276 }
277
278 /**
279 * This function is used to update the form action when switching from the advanced settings and quick-settings.
280 * @param {Object} target The submit button event target
281 * @return {void}
282 */
283 function switchAdvancedSettingsFormAction( target ) {
284 const form = document.querySelector( '#frm_styling_form' );
285 if ( null === form ) {
286 return;
287 }
288 if ( target.closest( 'a#frm_style_back_to_quick_settings' ) ) {
289 form.action = form.action.replace( '&section=advanced-settings', '' );
290 return;
291 }
292 if ( 'frm-style-advanced-settings-button' === target.id ) {
293 form.action += '&section=advanced-settings';
294 }
295 }
296
297 /**
298 * @return {void}
299 */
300 function disablePreviewSubmitButtons() {
301 const preview = document.getElementById( 'frm_style_preview' );
302 preview.querySelectorAll( 'form' ).forEach(
303 form => form.addEventListener(
304 'submit',
305 /**
306 * Prevent form submit event.
307 *
308 * @param {Event} event
309 * @return {false} Prevents the default action and stops the event from bubbling.
310 */
311 event => {
312 event.preventDefault();
313 event.stopPropagation();
314 return false;
315 }
316 )
317 );
318 }
319
320 /**
321 * @param {Event} event
322 * @return {void}
323 */
324 function handleClickEventsForListPage( event ) {
325 const target = event.target;
326
327 if ( target.classList.contains( 'frm-style-card' ) || target.closest( '.frm-style-card' ) ) {
328 handleStyleCardClick( event );
329 return; // eslint-disable-line
330 }
331 }
332
333 /**
334 * When a style card is clicked, the preview is updated.
335 * If the Update button is clicked after selecting a style card, the active card will be saved as the target form's style.
336 *
337 * @param {Event} event
338 * @return {void}
339 */
340 function handleStyleCardClick( event ) {
341 const target = event.target;
342
343 if ( target.closest( '.dropdown' ) ) {
344 // Ignore the hamburger menu inside of the card.
345 return;
346 }
347
348 const card = target.classList.contains( 'frm-style-card' ) ? target : target.closest( '.frm-style-card' );
349 const cardIsLocked = card.classList.contains( 'frm-locked-style' );
350
351 if ( cardIsLocked ) {
352 maybeCreateStyleTemplateModal( card );
353 return; // Exit early as we're not actually selecting a locked template for preview.
354 }
355
356 const previewArea = document.getElementById( 'frm_style_preview' );
357 const activeCard = document.querySelector( '.frm-active-style-card' );
358 const sampleForm = document.getElementById( 'frm_sample_form' ).querySelector( '.frm_forms' );
359 const styleIdInput = getStyleIdInput();
360
361 disableLabelTransitions();
362
363 activeCard.classList.remove( 'frm-active-style-card' );
364 card.classList.add( 'frm-active-style-card' );
365
366 const form = previewArea.querySelector( 'form' );
367 if ( form ) {
368 // If you do not have a valid form selected, form may be null.
369 form.parentNode.classList.remove( activeCard.dataset.classname );
370 form.parentNode.classList.add( card.dataset.classname );
371 }
372
373 sampleForm.classList.remove( activeCard.dataset.classname );
374 sampleForm.classList.add( card.dataset.classname );
375
376 if ( ! cardIsLocked ) {
377 // Don't update the form when a locked card is clicked.
378 styleIdInput.value = card.dataset.styleId;
379 trackListPageChange();
380 }
381
382 setTimeout( enableLabelTransitions, 1 );
383
384 // We want to toggle the edit button so you can only leave the page to edit the style if it's active (to avoid unsaved changes).
385 const editButton = document.getElementById( 'frm_edit_style' );
386 const showEditButton = null !== card.querySelector( '.frm-style-card-info' ); // Only the "Applied style" has card info.
387 editButton.classList.toggle( 'frm_hidden', ! showEditButton );
388
389 changeLabelPositionsInPreview( card.dataset.labelPosition );
390
391 // Trigger an action here so Pro can handle template preview updates on card click.
392 const hookName = 'frm_style_card_click';
393 const hookArgs = { card, styleIdInput };
394 wp.hooks.doAction( hookName, hookArgs );
395 }
396
397 /**
398 * @param {HTMLElement} card
399 * @return {HTMLElement} The modal element.
400 */
401 function maybeCreateStyleTemplateModal( card ) {
402 const titleElement = card.querySelector( '.frm-style-card-title' );
403 const templateTitle = titleElement.textContent;
404 const modal = maybeCreateModal(
405 'frm_style_template_modal',
406 {
407 content: getStyleTemplateModalContent( card ),
408 footer: getStyleTemplateModalFooter( card )
409 }
410 );
411 modal.querySelector( '.frm-modal-title' ).textContent = templateTitle;
412 return modal;
413 }
414
415 /**
416 * @param {HTMLElement} card
417 * @return {HTMLElement} The modal content element.
418 */
419 function getStyleTemplateModalContent( card ) {
420 const children = [];
421
422 children.push(
423 div( {
424 className: 'frm_warning_style',
425 children: [
426 span(
427 /* translators: %s: The required license type (ie. Plus, Business, or Elite) */
428 sprintf( __( 'Access to this style requires the %s plan.', 'formidable' ), card.dataset.requires )
429 ),
430 a( {
431 text: getUpgradeNowText(),
432 href: card.dataset.upgradeUrl,
433 target: '_blank'
434 } )
435 ]
436 } )
437 );
438
439 return div( { children } );
440 }
441
442 /**
443 * @param {HTMLElement} card
444 * @return {HTMLElement} The modal footer element.
445 */
446 function getStyleTemplateModalFooter( card ) {
447 const viewDemoSiteButton = footerButton( {
448 text: __( 'Learn More', 'formidable' ),
449 buttonType: 'secondary'
450 } );
451 viewDemoSiteButton.href = card.dataset.upgradeUrl;
452 viewDemoSiteButton.target = '_blank';
453
454 const primaryActionButton = footerButton( {
455 text: getUpgradeNowText(),
456 buttonType: 'primary'
457 } );
458
459 primaryActionButton.classList.remove( 'dismiss' );
460 primaryActionButton.setAttribute( 'href', card.dataset.upgradeUrl );
461 primaryActionButton.target = '_blank';
462
463 return div( {
464 children: [ viewDemoSiteButton, primaryActionButton ]
465 } );
466 }
467
468 /**
469 * @return {string} The upgrade now text.
470 */
471 function getUpgradeNowText() {
472 return __( 'Upgrade Now', 'formidable' );
473 }
474
475 /**
476 * Track an unsaved change on the edit page.
477 * This is included in the frmStylerFunctions global so unsaved changes can be tracked in Pro as well.
478 *
479 * @return {void}
480 */
481 function trackUnsavedChange() {
482 state.unsavedChanges = true;
483 }
484
485 /**
486 * Possibly prevent leaving the page if there are unsaved changes.
487 *
488 * @param {Event} event
489 * @return {void}
490 */
491 function maybeConfirmExit( event ) {
492 if ( ! state.unsavedChanges ) {
493 return;
494 }
495
496 event.preventDefault();
497 event.returnValue = '';
498 }
499
500 /**
501 * Floating labels have a transition style. Turn it off temporarily when switching between cards to avoid a transition between two different style classes.
502 *
503 * @return {void}
504 */
505 function disableLabelTransitions() {
506 setLabelTransitionStyle( 'none' );
507 }
508
509 /**
510 * @return {void}
511 */
512 function enableLabelTransitions() {
513 setLabelTransitionStyle( '' );
514 }
515
516 /**
517 * @param {string} value
518 * @return {void}
519 */
520 function setLabelTransitionStyle( value ) {
521 document.getElementById( 'frm_style_preview' ).querySelectorAll( '.frm_inside_container' ).forEach(
522 container => container.querySelector( 'label' ).style.transition = value
523 );
524 }
525
526 /**
527 * @return {void}
528 */
529 function toggleSampleForm() {
530 state.showingSampleForm = ! state.showingSampleForm;
531 document.getElementById( 'frm_active_style_form' ).classList.toggle( 'frm_hidden', state.showingSampleForm );
532 document.getElementById( 'frm_toggle_sample_form' ).querySelector( 'span' ).textContent = state.showingSampleForm ? __( 'View my form', 'formidable' ) : __( 'View sample form', 'formidable' );
533 }
534
535 /**
536 * @return {void}
537 */
538 function handleUpdateClick() {
539 state.unsavedChanges = false; // Prevent the saved changes pop up from triggering when submitting the form.
540
541 const form = document.getElementById( 'frm_styling_form' );
542 if ( form ) {
543 // Submitting for an "edit" view.
544 form.submit();
545 return;
546 }
547
548 document.getElementById( 'frm_submit_side_top' ).classList.add( 'frm_loading_button' );
549
550 // Submit the "list" view (assign a style to a form).
551 document.getElementById( 'frm_style_list_form' ).submit();
552 }
553
554 /**
555 * Maybe modify an anchor's URL on click.
556 * If the sample form toggle is active, we want to pass that as a query parameter so we know to default to the sample form on load.
557 *
558 * @param {HTMLElement} clickTarget
559 * @return {void}
560 */
561 function modifyStylerUrl( clickTarget ) {
562 if ( ! state.showingSampleForm ) {
563 // Don't change the URL if it is not a sample form.
564 return;
565 }
566
567 const anchor = clickTarget.hasAttribute( 'href' ) ? clickTarget : clickTarget.querySelector( 'a[href]' );
568 anchor.setAttribute( 'href', anchor.getAttribute( 'href' ) + '&sample=1' );
569 }
570
571 /**
572 * Add menu dropdowns to style cards dynamically on load.
573 *
574 * @return {void}
575 */
576 function addHamburgerMenusToCards() {
577 const cards = Array.from( document.getElementsByClassName( 'frm-style-card' ) );
578 cards.forEach( card => maybeAddMenuToCard( card ) );
579 }
580
581 /**
582 * @param {HTMLElement} card
583 * @return {void}
584 */
585 function maybeAddMenuToCard( card ) {
586 if ( ! shouldAddMenuToCard( card ) ) {
587 return;
588 }
589
590 card.appendChild( getHamburgerMenu( card.dataset ) );
591 }
592
593 /**
594 * Avoid adding a menu to an upsell card or a template card.
595 *
596 * @param {HTMLElement} card
597 * @return {boolean} Whether to add a menu to the card.
598 */
599 function shouldAddMenuToCard( card ) {
600 return 'frm_template_style_cards_wrapper' !== card.parentNode.id || ! card.classList.contains( 'frm-locked-style' );
601 }
602
603 /**
604 * @return {void}
605 */
606 function addHamburgerMenuForEditPage() {
607 const styleName = document.getElementById( 'frm_style_name' );
608 if ( ! styleName ) {
609 return;
610 }
611
612 const styleId = document.getElementById( 'frm_styling_form' ).querySelector( 'input[name="ID"]' ).value;
613
614 const hamburgerMenu = getHamburgerMenu( { styleId } );
615 hamburgerMenu.classList.add( 'alignright' );
616 styleName.parentNode.insertBefore( hamburgerMenu, styleName );
617 }
618
619 /**
620 * Get a dropdown and the "hamburger" stacked dot menu trigger for a single style card.
621 *
622 * @param {DOMStringMap} data {
623 * @type {string} editUrl
624 * @type {string} styleId
625 * @type {string} labelPosition
626 * @type {string} classname
627 * }
628 * @return {HTMLElement} The hamburger menu element.
629 */
630 function getHamburgerMenu( data ) {
631 const hamburgerMenu = a( {
632 className: 'frm-dropdown-toggle dropdown-toggle',
633 child: svg( { href: '#frm_thick_more_vert_icon' } )
634 } );
635 hamburgerMenu.setAttribute( 'data-toggle', 'dropdown' );
636 hamburgerMenu.setAttribute( 'data-container', 'body' );
637 hamburgerMenu.setAttribute( 'role', 'button' );
638 hamburgerMenu.setAttribute( 'tabindex', 0 );
639
640 const isTemplate = 'undefined' !== typeof data.templateKey;
641 let dropdownMenuOptions = [];
642
643 if ( isListPage ) {
644 const applyOption = a( {
645 text: isTemplate ? __( 'Install and apply', 'formidable' ) : __( 'Apply', 'formidable' )
646 } );
647 addIconToOption( applyOption, 'frm_save_icon' );
648 dropdownMenuOptions.push( { anchor: applyOption, type: 'apply' } );
649 onClickPreventDefault( applyOption, handleApplyOptionClick );
650 }
651
652 if ( ! isTemplate ) {
653 if ( 'string' === typeof data.editUrl ) {
654 // The Edit option is not included on the Edit page.
655 const editOption = a( {
656 text: __( 'Edit', 'formidable' ),
657 href: data.editUrl
658 } );
659 addIconToOption( editOption, 'frm_pencil_icon' );
660 dropdownMenuOptions.push( { anchor: editOption, type: 'edit' } );
661 }
662
663 const resetOption = a( {
664 text: __( 'Reset to Defaults', 'formidable' )
665 } );
666 addIconToOption( resetOption, 'frm_repeater_icon' );
667 onClickPreventDefault( resetOption, () => confirmResetStyle( data.styleId ) );
668
669 dropdownMenuOptions.push(
670 { anchor: getRenameOption( data.styleId ), type: 'rename' },
671 { anchor: resetOption, type: 'reset' }
672 );
673 }
674
675 const hookName = 'frm_style_card_dropdown_options';
676 const hookArgs = { data, addIconToOption, isTemplate };
677 dropdownMenuOptions = wp.hooks.applyFilters( hookName, dropdownMenuOptions, hookArgs );
678
679 if ( isListPage && ! isTemplate ) {
680 maybeAddDuplicateUpsell( dropdownMenuOptions );
681 }
682
683 const dropdownMenu = div( {
684 // Use dropdown-menu-right to avoid an overlapping issue with the card to the right (where the # of forms would appear above the menu).
685 className: 'frm-dropdown-menu frm-style-options-menu frm-p-1',
686 children: dropdownMenuOptions.map( wrapDropdownItem )
687 } );
688
689 const isRtl = document.body.classList.contains( 'rtl' );
690 dropdownMenu.classList.add( 'dropdown-menu-' + ( isRtl ? 'left' : 'right' ) );
691
692 dropdownMenu.setAttribute( 'role', 'menu' );
693
694 return div( {
695 className: 'dropdown frm_wrap', // The .frm_wrap class prevents a blue outline on the active dropdown trigger.
696 children: [ hamburgerMenu, dropdownMenu ]
697 } );
698 }
699
700 /**
701 * @param {Array} dropdownMenuOptions
702 * @return {void}
703 */
704 function maybeAddDuplicateUpsell( dropdownMenuOptions ) {
705 let duplicateOptionExists = false;
706 for ( let i = 0; i < dropdownMenuOptions.length; ++i ) {
707 if ( dropdownMenuOptions[ i ].type === 'duplicate' ) {
708 duplicateOptionExists = true;
709 break;
710 }
711 }
712
713 if ( duplicateOptionExists ) {
714 return;
715 }
716
717 const duplicateUpsell = a( {
718 text: __( 'Duplicate', 'formidable' ),
719 className: 'frm_noallow'
720 } );
721 addIconToOption( duplicateUpsell, 'frm_clone_icon' );
722 onClickPreventDefault( duplicateUpsell, () => document.getElementById( 'frm_new_style_trigger' ).click() );
723 const upsellOption = { anchor: duplicateUpsell, type: 'duplicate' };
724 dropdownMenuOptions.splice( 3, 0, upsellOption );
725 }
726
727 /**
728 * @param {Event} event
729 * @return {void}
730 */
731 function handleApplyOptionClick( event ) {
732 const option = event.target;
733 const card = option.closest( '.frm-style-card' );
734 if ( ! card ) {
735 return;
736 }
737
738 card.click();
739 handleUpdateClick();
740 }
741
742 /**
743 * @param {string} styleId
744 * @return {HTMLElement} The rename option element.
745 */
746 function getRenameOption( styleId ) {
747 const renameOption = a( __( 'Rename', 'formidable' ) );
748 addIconToOption( renameOption, 'frm_signature2_icon' );
749
750 let titleTarget;
751
752 // Depending on the page we're pulling the text from an existing element on the page.
753 if ( isListPage ) {
754 titleTarget = getCardByStyleId( styleId ).querySelector( '.frm-style-card-title' );
755 } else {
756 titleTarget = document.getElementById( 'frm_style_name' );
757 }
758
759 onClickPreventDefault(
760 renameOption,
761 () => {
762 const styleName = titleTarget.textContent;
763 stylerModal(
764 'frm_rename_style_modal',
765 {
766 title: __( 'Rename style', 'formidable' ),
767 content: getStyleInputNameModalContent( 'rename', styleName ),
768 footer: getRenameStyleModalFooter( styleId )
769 }
770 );
771 }
772 );
773
774 return renameOption;
775 }
776
777 /**
778 * @param {string} id
779 * @param {Object} args
780 * @return {HTMLElement} The modal element.
781 */
782 function stylerModal( id, args ) {
783 const modal = maybeCreateModal( id, args );
784 // Include both wp-core-ui and frm-white-body on the modal.
785 // Without wp-core-ui, the vertical alignment of the primary button is wrong.
786 // Without frm-white-body, cancel buttons in the modal do not get styled properly.
787 modal.classList.add( 'frm_common_modal', 'wp-core-ui', 'frm-white-body' );
788 return modal;
789 }
790
791 /**
792 * Get modal content with just a "Style Name" input.
793 * This is used for New style, Duplicate style, and for Rename style.
794 *
795 * @param {string} context
796 * @param {string|undefined} value
797 * @return {HTMLElement} The modal content element.
798 */
799 function getStyleInputNameModalContent( context, value ) {
800 // Create a form so we can listen to Enter key presses that trigger a form submit event.
801 const form = tag(
802 'form',
803 {
804 child: labelledTextInput( 'frm_' + context + '_style_name_input', __( 'Style name', 'formidable' ), 'style_name' )
805 }
806 );
807 form.addEventListener(
808 'submit',
809 /**
810 * @param {Event} event
811 * @return {false} Prevents the default action and stops the event from bubbling.
812 */
813 event => {
814 // Prevent the form in the modal from submitting and trigger the click button in the modal footer instead.
815 event.preventDefault();
816
817 const modal = form.closest( '.frm-dialog' );
818 modal.querySelector( '.frm_modal_footer .frm-button-primary' ).click();
819
820 return false;
821 }
822 );
823 const content = div( { child: form } );
824 content.style.padding = '20px';
825 content.querySelector( 'label' ).style.lineHeight = 1.5;
826
827 const styleNameInput = content.querySelector( 'input' );
828 styleNameInput.addEventListener(
829 'input',
830 () => {
831 const footerSubmitButton = styleNameInput.closest( '.frm_modal_content' ).nextElementSibling.querySelector( '.frm-button-primary' );
832 if ( '' === styleNameInput.value ) {
833 footerSubmitButton.setAttribute( 'disabled', 'disabled' );
834 footerSubmitButton.classList.remove( 'dismiss' );
835 } else {
836 footerSubmitButton.removeAttribute( 'disabled' );
837 footerSubmitButton.classList.add( 'dismiss' );
838 }
839 }
840 );
841
842 if ( 'string' === typeof value ) {
843 styleNameInput.value = value;
844 }
845
846 return content;
847 }
848
849 /**
850 * @param {string} styleId
851 * @return {HTMLElement} The modal footer element.
852 */
853 function getRenameStyleModalFooter( styleId ) {
854 const cancelButton = footerButton( { text: __( 'Cancel', 'formidable' ), buttonType: 'cancel' } );
855 cancelButton.classList.add( 'dismiss' );
856
857 const renameButton = footerButton( { text: __( 'Rename style', 'formidable' ), buttonType: 'primary' } );
858 onClickPreventDefault( renameButton, () => renameStyle( styleId ) );
859
860 return div( {
861 children: [ cancelButton, renameButton ]
862 } );
863 }
864
865 /**
866 * Call frm_rename_style action when the rename style button is clicked in rename modal.
867 *
868 * @param {string} styleId
869 * @return {void}
870 */
871 function renameStyle( styleId ) {
872 const styleNameInput = document.getElementById( 'frm_rename_style_name_input' );
873 const newStyleName = styleNameInput.value;
874
875 if ( '' === newStyleName ) {
876 // Avoid setting an empty name.
877 // The button gets disabled on an input event when the name is empty.
878 return;
879 }
880
881 const formData = new FormData();
882 formData.append( 'style_id', styleId );
883 formData.append( 'style_name', newStyleName );
884 doJsonPost( 'rename_style', formData ).then(
885 /**
886 * Sync the page with the new name of renamed style after successfully making a POST request.
887 *
888 * If on the list page, update the style card after renaming a style.
889 * On the edit page, update the style name element instead.
890 *
891 * @return {void}
892 */
893 () => {
894 success( __( 'Style has been renamed successfully', 'formidable' ) );
895
896 if ( isListPage ) {
897 updateStyleNameInCard( styleId, newStyleName );
898 return;
899 }
900
901 const titleSpan = document.getElementById( 'frm_style_name' );
902 titleSpan.textContent = newStyleName;
903 }
904 );
905 }
906
907 /**
908 * @param {string} styleId
909 * @param {string} newStyleName
910 * @return {void}
911 */
912 function updateStyleNameInCard( styleId, newStyleName ) {
913 const card = getCardByStyleId( styleId );
914 const titleElement = card.querySelector( '.frm-style-card-title' );
915 titleElement.textContent = newStyleName;
916 }
917
918 /**
919 * @param {string} templateKey
920 * @return {HTMLElement} The template card element.
921 */
922 function getTemplateCard( templateKey ) {
923 return document.getElementById( 'frm_template_style_cards_wrapper' ).querySelector( '.frm-style-card[data-template-key="' + templateKey + '"]' );
924 }
925
926 /**
927 * @param {string} styleId
928 * @return {HTMLElement} The card element.
929 */
930 function getCardByStyleId( styleId ) {
931 const defaultCard = document.querySelector( '#frm_default_style_cards_wrapper > div[data-style-id="' + styleId + '"]' );
932 if ( defaultCard ) {
933 return defaultCard;
934 }
935 return Array.from( document.getElementById( 'frm_custom_style_cards_wrapper' ).children ).find( card => card.dataset.styleId === styleId );
936 }
937
938 /**
939 * @param {HTMLElement} option
940 * @param {string} iconId
941 * @return {void}
942 */
943 function addIconToOption( option, iconId ) {
944 const icon = frmDom.svg( { href: '#' + iconId } );
945 option.insertBefore( icon, option.firstChild );
946 }
947
948 /**
949 * @param {string} styleId
950 * @return {void}
951 */
952 function confirmResetStyle( styleId ) {
953 stylerModal(
954 'frm_reset_style_modal',
955 {
956 title: __( 'Reset style', 'formidable' ),
957 content: getResetStyleModalContent(),
958 footer: getResetStyleModalFooter( styleId )
959 }
960 );
961 }
962
963 /**
964 * @return {HTMLElement} The modal content element.
965 */
966 function getResetStyleModalContent() {
967 const content = div( __( 'Reset this style back to the default?', 'formidable' ) );
968 content.style.padding = '20px';
969 return content;
970 }
971
972 /**
973 * @param {string} styleId
974 * @return {HTMLElement} The modal footer element.
975 */
976 function getResetStyleModalFooter( styleId ) {
977 const cancelButton = footerButton( {
978 text: __( 'Cancel', 'formidable' ),
979 buttonType: 'cancel'
980 } );
981 cancelButton.classList.add( 'dismiss' );
982 const resetButton = footerButton( {
983 text: __( 'Reset style', 'formidable' ),
984 buttonType: 'primary'
985 } );
986 onClickPreventDefault( resetButton, () => resetStyle( styleId ) );
987 return div( { children: [ cancelButton, resetButton ] } );
988 }
989
990 /**
991 * Handle reset dropdown action.
992 * This function handles the front end routing for the reset action as reset works differently for edit and list views.
993 *
994 * @param {string} styleId
995 * @return {void}
996 */
997 function resetStyle( styleId ) {
998 if ( isListPage ) {
999 resetStyleOnListPage( styleId );
1000 return;
1001 }
1002 resetStyleOnEditPage();
1003 }
1004
1005 /**
1006 * Make a POST request to reset the style then reload the CSS and reset the card styles.
1007 *
1008 * @param {string} styleId
1009 * @return {void}
1010 */
1011 function resetStyleOnListPage( styleId ) {
1012 const formData = new FormData();
1013 formData.append( 'style_id', styleId );
1014 doJsonPost( 'settings_reset', formData ).then(
1015 response => {
1016 const card = getCardByStyleId( styleId );
1017 card.classList.remove( 'frm-dark-style' );
1018 if ( 'string' === typeof response.style ) {
1019 card.style = response.style;
1020 }
1021 reloadCSSAfterStyleReset();
1022 showStyleResetSuccessMessage();
1023 }
1024 );
1025 }
1026
1027 function showStyleResetSuccessMessage() {
1028 success( __( 'Style has been reset successfully', 'formidable' ) );
1029 }
1030
1031 /**
1032 * Reset the style in-page (without actually updating it).
1033 *
1034 * @return {void}
1035 */
1036 function resetStyleOnEditPage() {
1037 jQuery.ajax( {
1038 type: 'POST',
1039 url: ajaxurl,
1040 data: {
1041 action: 'frm_settings_reset',
1042 nonce: frmGlobal.nonce
1043 },
1044 success: syncEditPageAfterResetAction
1045 } );
1046 }
1047
1048 /**
1049 * Handle reset success on edit page.
1050 * This function sets all styling inputs to default values.
1051 *
1052 * @todo Stop triggering change events with jQuery. And remove the other jQuery as well.
1053 *
1054 * @param {Object} response
1055 * @return {void}
1056 */
1057 function syncEditPageAfterResetAction( response ) {
1058 let defaultValues = response.replace( /^\s+|\s+$/g, '' );
1059 if ( defaultValues.indexOf( '{' ) === 0 ) {
1060 defaultValues = JSON.parse( defaultValues );
1061 }
1062
1063 for ( const key in defaultValues ) {
1064 let targetInput = document.querySelector( 'input[name$="[' + key + ']"], select[name$="[' + key + ']"]' );
1065 if ( ! targetInput ) {
1066 continue;
1067 }
1068
1069 if ( 'radio' === targetInput.getAttribute( 'type' ) ) {
1070 // Reset the repeater icon dropdown.
1071 targetInput = document.querySelector( 'input[name$="[' + key + ']"][value="' + defaultValues[ key ] + '"]' );
1072 if ( targetInput ) {
1073 targetInput.checked = true;
1074 jQuery( targetInput ).trigger( 'change' );
1075 }
1076 continue;
1077 }
1078
1079 targetInput.value = defaultValues[ key ];
1080
1081 if ( targetInput.classList.contains( 'wp-color-picker' ) ) {
1082 // Trigger a change event so the color pickers sync. Otherwise they stay the same color after reset.
1083 jQuery( targetInput ).trigger( 'change' );
1084 }
1085 }
1086
1087 jQuery( '#frm_submit_style, #frm_auto_width' ).prop( 'checked', false );
1088 jQuery( document.getElementById( 'frm_fieldset' ) ).trigger( 'change' );
1089 showStyleResetSuccessMessage();
1090 }
1091
1092 /**
1093 * Reload Formidable CSS after a style is reset so the preview updates immediately without needing to reload the page.
1094 *
1095 * @return {void}
1096 */
1097 function reloadCSSAfterStyleReset() {
1098 const style = document.getElementById( 'frm-custom-theme-css' );
1099 if ( ! style ) {
1100 return;
1101 }
1102
1103 const newStyle = document.createElement( 'link' );
1104 newStyle.rel = 'stylesheet';
1105 newStyle.type = 'text/css';
1106 newStyle.href = style.href + '&key=' + getAutoId(); // Make the URL unique so the old stylesheet doesn't get picked up by cache.
1107
1108 // Listen for the new style to load before removing the old style to avoid having no styles while the new style is loading.
1109 newStyle.addEventListener(
1110 'load',
1111 () => {
1112 style.parentNode.removeChild( style );
1113 newStyle.id = 'frm-custom-theme-css'; // Assign the old ID to the new style so it can be removed in the next reset action.
1114 }
1115 );
1116
1117 const head = document.getElementsByTagName( 'HEAD' )[ 0 ];
1118 head.appendChild( newStyle );
1119 }
1120
1121 /**
1122 * @return {number} The auto ID.
1123 */
1124 function getAutoId() {
1125 return ++state.autoId;
1126 }
1127
1128 /**
1129 * @param {Object} data
1130 * @param {HTMLElement} data.anchor
1131 * @param {string} data.type
1132 * @return {HTMLElement} The dropdown item element.
1133 */
1134 function wrapDropdownItem( { anchor, type } ) {
1135 return div( {
1136 className: 'dropdown-item frm-' + type + '-style',
1137 child: anchor
1138 } );
1139 }
1140
1141 /**
1142 * This gets triggered through a hook called in frmAdminBuild.styleInit() from formidable_admin.js.
1143 *
1144 * @return {void}
1145 */
1146 function initEditPage() {
1147 const { debounce } = frmDom.util;
1148 const debouncedPreviewUpdate = debounce( () => changeStyling(), 100 );
1149 const debouncedColorChange = debounce( ( event, value ) => {
1150 /**
1151 * Fires on style colorpicker change.
1152 *
1153 * @param {Event} data.event The color change event.
1154 * @param {string} data.value New color value.
1155 */
1156 wp.hooks.doAction( 'frm_style_options_color_change', { event, value } );
1157 }, 200 );
1158
1159 const debouncedTextSquishCheck = debounce( textSquishCheck, 300 );
1160 initPosClass(); // It's important that this gets called before we add event listeners because it triggers change events.
1161
1162 [ 'frm_field_height', 'frm_field_font_size', 'frm_field_pad' ].forEach( selector => {
1163 document.getElementById( selector ).addEventListener( 'change', debouncedTextSquishCheck );
1164 } );
1165
1166 jQuery( 'input.hex' ).wpColorPicker( {
1167 change: function( event, ui ) {
1168 let color = jQuery( this ).wpColorPicker( 'color' );
1169 trackUnsavedChange();
1170 if ( ui.color._alpha < 1 ) {
1171 // If there's transparency, use RGBA
1172 color = ui.color.toCSS( 'rgba' );
1173 }
1174 debouncedColorChange( event, color );
1175
1176 if ( null !== event.target.getAttribute( 'data-alpha-color-type' ) ) {
1177 debouncedPreviewUpdate();
1178 return;
1179 }
1180
1181 jQuery( event.target ).val( color ).trigger( 'change' );
1182 }
1183 } );
1184 jQuery( '.wp-color-result-text' ).text( function( _, oldText ) {
1185 const container = jQuery( this ).closest( '.wp-picker-container' );
1186 if ( 'undefined' !== typeof container && container[ 0 ].parentElement.classList.contains( 'frm-colorpicker' ) ) {
1187 return container[ 0 ].querySelector( '.wp-color-picker' ).value;
1188 }
1189 return oldText === 'Select Color' ? 'Select' : oldText;
1190 } );
1191 jQuery( '#frm_styling_form .styling_settings, #frm_styling_form .frm-field-shape, #frm_styling_form input[name="frm_style_setting[post_content][base_font_size]"]' ).on( 'change', debouncedPreviewUpdate );
1192
1193 // This is really only necessary for Pro. But if Pro is not up to date to initialize the datepicker in the sample form, it should still work because it's initialized here.
1194 initDatepickerSample();
1195
1196 addHamburgerMenuForEditPage();
1197
1198 document.getElementById( 'frm_styling_form' ).querySelectorAll( 'input, select' ).forEach(
1199 input => input.addEventListener( 'change', () => trackUnsavedChange() )
1200 );
1201
1202 /**
1203 * Sends an AJAX POST request for new CSS to use for the preview.
1204 * This is called whenever a style setting is changed, generally using debouncedPreviewUpdate to avoid simultaneous requests.
1205 *
1206 * @return {void}
1207 */
1208 function changeStyling() {
1209 const styleInputs = Array.from( document.getElementById( 'frm_style_sidebar' ).querySelectorAll( 'input, select, textarea' ) ).filter(
1210 input => 'style_name' === input.name || 0 === input.name.indexOf( 'frm_style_setting[post_content]' )
1211 );
1212 const locStr = JSON.stringify( jQuery( styleInputs ).serializeArray() );
1213
1214 jQuery.ajax( {
1215 type: 'POST',
1216 url: ajaxurl,
1217 data: {
1218 action: 'frm_change_styling',
1219 nonce: frmGlobal.nonce,
1220 frm_style_setting: locStr
1221 },
1222 success: css => {
1223 handleChangeStylingSuccess( css );
1224 setSelectPlaceholderColor();
1225 }
1226 } );
1227 }
1228
1229 /**
1230 * Update the CSS used for the preview on the edit page when a styling input has been updated.
1231 *
1232 * @param {string} css The response from the frm_change_styling request.
1233 * @return {void}
1234 */
1235 function handleChangeStylingSuccess( css ) {
1236 // Validate the string response. A valid output will include rules with .with_frm_style
1237 if ( -1 === css.indexOf( '.with_frm_style' ) ) {
1238 // Handle error (possibly a permission error, or an outdated nonce).
1239 alert( css );
1240 return;
1241 }
1242 document.getElementById( 'this_css' ).innerHTML = css;
1243 }
1244
1245 /**
1246 * Possibly pop up with a warning that "text will not display correctly if the field height is too small relative to the field padding and text size".
1247 * This can be triggered when modifying font size, height, and padding.
1248 *
1249 * @return {void}
1250 */
1251 function textSquishCheck() {
1252 if ( null !== frmDom.util.getCookie( 'frm-style-text-squish-check' ) ) {
1253 return;
1254 }
1255 const height = document.getElementById( 'frm_field_height' ).value.replace( /\D/g, '' );
1256 const paddingEntered = document.getElementById( 'frm_field_pad' ).value.split( ' ' );
1257 const paddingCount = paddingEntered.length;
1258
1259 frmDom.util.setCookie( 'frm-style-text-squish-check', 1, 30 );
1260
1261 // If too many or too few padding entries, leave now
1262 if ( paddingCount === 0 || paddingCount > 4 || height === '' ) {
1263 return;
1264 }
1265
1266 const size = document.getElementById( 'frm_field_font_size' ).value.replace( /\D/g, '' );
1267 // Get the top and bottom padding from entered values
1268 const paddingTop = paddingEntered[ 0 ].replace( /\D/g, '' );
1269 let paddingBottom = paddingTop;
1270 if ( paddingCount >= 3 ) {
1271 paddingBottom = paddingEntered[ 2 ].replace( /\D/g, '' );
1272 }
1273
1274 // Check if there is enough space for text
1275 const textSpace = height - size - paddingTop - paddingBottom - 3;
1276 if ( textSpace < 0 ) {
1277 frmAdminBuild.infoModal( frm_admin_js.css_invalid_size );
1278 }
1279 }
1280
1281 /**
1282 * When the Collapse icons are updated, sync the dropdown.
1283 * Otherwise the previously selected value will still appear as the selected value.
1284 *
1285 * @return {void}
1286 */
1287 jQuery( document ).on( 'change', '.frm-dropdown-menu input[type="radio"]', function() {
1288 trackUnsavedChange();
1289
1290 const radio = this;
1291 const btnGrp = radio.closest( '.btn-group' );
1292 const btnId = btnGrp.getAttribute( 'id' );
1293
1294 const select = document.getElementById( btnId.replace( '_select', '' ) );
1295 if ( select ) {
1296 select.value = radio.value;
1297 }
1298
1299 jQuery( btnGrp ).children( 'button' ).html( radio.nextElementSibling.innerHTML + ' <b class="caret"></b>' );
1300
1301 const activeItem = btnGrp.querySelector( '.dropdown-item.active' );
1302 if ( activeItem ) {
1303 activeItem.classList.remove( 'active' );
1304 }
1305
1306 radio.closest( '.dropdown-item' ).classList.add( 'active' );
1307 } );
1308
1309 if ( frm_admin_js.requireAccordionTitleClickListener ) {
1310 document.querySelectorAll( '.styling_settings h3.accordion-section-title' ).forEach( el => {
1311 el.addEventListener( 'click', event => {
1312 if ( ! event.target.closest( 'button' ) ) {
1313 el.querySelector( 'button' ).click();
1314 }
1315 } );
1316 } );
1317 }
1318 }
1319
1320 /**
1321 * @param {Event} event
1322 */
1323 function maybeCollapseSettings( event ) {
1324 let expanded;
1325 const sectionParent = event.target.parentElement;
1326 if ( event.type === 'keydown' ) {
1327 expanded = sectionParent.classList.toggle( 'open' );
1328 jQuery( sectionParent.querySelector( '.accordion-section-content' ) ).toggle( ! expanded ).slideToggle( 150 ); // Animate toggle as in click/enter.
1329 } else {
1330 expanded = sectionParent.classList.contains( 'open' );
1331 }
1332
1333 event.target.setAttribute( 'aria-expanded', expanded );
1334 }
1335
1336 /**
1337 * @param {HTMLElement} input
1338 * @param {HTMLElement} container
1339 * @return {void}
1340 */
1341 function checkFloatingLabelsForStyles( input, container ) {
1342 if ( ! container ) {
1343 container = input.closest( '.frm_inside_container' );
1344 }
1345
1346 const shouldFloatTop = input.value || document.activeElement === input;
1347
1348 container.classList.toggle( 'frm_label_float_top', shouldFloatTop );
1349
1350 if ( 'SELECT' !== input.tagName ) {
1351 return;
1352 }
1353
1354 const firstOpt = input.querySelector( 'option:first-child' );
1355
1356 if ( shouldFloatTop ) {
1357 if ( firstOpt.hasAttribute( 'data-label' ) ) {
1358 firstOpt.textContent = firstOpt.getAttribute( 'data-label' );
1359 firstOpt.removeAttribute( 'data-label' );
1360 }
1361 } else if ( firstOpt.textContent ) {
1362 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1363 firstOpt.textContent = '';
1364 }
1365 }
1366
1367 /**
1368 * @return {void}
1369 */
1370 function initPosClass() {
1371 const positionSetting = document.getElementById( 'frm_position' );
1372
1373 jQuery( positionSetting ).on( 'change', setPosClass );
1374
1375 // Trigger label position option on load.
1376 const changeEvent = document.createEvent( 'HTMLEvents' );
1377 changeEvent.initEvent( 'change', true, false );
1378 positionSetting.dispatchEvent( changeEvent );
1379 }
1380
1381 /**
1382 * Update label container classes when the label "Position" setting is changed.
1383 *
1384 * @return {void}
1385 */
1386 function setPosClass() {
1387 /*jshint validthis:true */
1388 let value = this.value;
1389 if ( value === 'none' ) {
1390 value = 'top';
1391 } else if ( value === 'no_label' ) {
1392 value = 'none';
1393 }
1394
1395 document.getElementById( 'frm_style_preview' ).querySelectorAll( '.frm_form_field.frm-default-label-position, #frm_sample_form .frm_form_field' ).forEach( container => {
1396 const input = container.querySelector( ':scope > input, :scope > select, :scope > textarea' ); // Fields that support floating label should have a directly child input/textarea/select.
1397 const shouldForceTopStyling = 'inside' === value && ( ! input || 'hidden' === input.type ); // We do not want file upload to use floating labels, or inline datepickers, which both use hidden inputs.
1398 const currentValue = shouldForceTopStyling ? 'top' : value;
1399
1400 container.classList.remove( 'frm_top_container', 'frm_left_container', 'frm_right_container', 'frm_none_container', 'frm_inside_container' );
1401 container.classList.add( 'frm_' + currentValue + '_container' );
1402
1403 if ( 'inside' === currentValue ) {
1404 checkFloatingLabelsForStyles( input, container );
1405 }
1406 } );
1407 }
1408
1409 /**
1410 * @return {void}
1411 */
1412 function initFloatingLabels() {
1413 [ 'focus', 'blur', 'change' ].forEach(
1414 eventName => frmDom.util.documentOn(
1415 eventName,
1416 '#frm_style_preview .frm_inside_container > input, #frm_style_preview .frm_inside_container > textarea, #frm_style_preview .frm_inside_container > select',
1417 event => checkFloatingLabelsForStyles( event.target ),
1418 true
1419 )
1420 );
1421 }
1422
1423 /**
1424 * The signature add on expects that validateFormSubmit is callable.
1425 * Without this, drawing in a signature field triggers a "Uncaught ReferenceError: frmFrontForm is not defined" error.
1426 * We don't want the validation to actually triggr, so just fill in an empty function.
1427 *
1428 * @return {void}
1429 */
1430 function fillMissingSignatureValidationFunction() {
1431 if ( 'undefined' === typeof window.__FRMSIG || 'undefined' !== typeof window.frmFrontForm ) {
1432 return;
1433 }
1434
1435 window.frmFrontForm = { validateFormSubmit: () => {} };
1436 }
1437
1438 /**
1439 * Enable the datepicker in the sample form preview.
1440 *
1441 * @return {void}
1442 */
1443 function initDatepickerSample() {
1444 // If flatpickr is defined, then is a recent version of Pro which handles the datepicker preview as it's a PRO feature.
1445 if ( 'undefined' !== typeof flatpickr ) {
1446 return;
1447 }
1448
1449 const $sample = jQuery( '#datepicker_sample' );
1450 if ( $sample.length && 'function' === typeof $sample.datepicker ) {
1451 $sample.datepicker( { changeMonth: true, changeYear: true } );
1452 }
1453 }
1454
1455 /**
1456 * Set color for select placeholders.
1457 *
1458 * @since 6.5.1
1459 */
1460 function setSelectPlaceholderColor() {
1461 const selects = document.querySelectorAll( '.form-field select' );
1462 const styleElement = document.querySelector( '.with_frm_style' );
1463 const textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : '';
1464
1465 // Exit if there are no select elements or the textColorDisabled property is missing
1466 if ( ! selects.length || ! textColorDisabled ) {
1467 return;
1468 }
1469
1470 // Function to change the color of a select element
1471 const changeSelectColor = select => {
1472 if ( select.options[ select.selectedIndex ] && select.options[ select.selectedIndex ].classList.contains( 'frm-select-placeholder' ) ) {
1473 select.style.setProperty( 'color', textColorDisabled, 'important' );
1474 } else {
1475 select.style.color = '';
1476 }
1477 };
1478
1479 // Use a loop to iterate through each select element
1480 selects.forEach( select => {
1481 // Apply the color change to each select element
1482 changeSelectColor( select );
1483
1484 // Add an event listener for future changes
1485 select.addEventListener( 'change', () => changeSelectColor( select ) );
1486 } );
1487 }
1488
1489 // Hook into the styleInit function in formidable_admin.js
1490 wp.hooks.addAction( 'frm_style_editor_init', 'formidable', initEditPage );
1491
1492 // Set a global object so these functions can be re-used in Pro.
1493 window.frmStylerFunctions = { getCardByStyleId, getStyleInputNameModalContent, trackUnsavedChange, stylerModal };
1494 }() );
1495