PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.28
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.28
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.28, at js/admin/style.js

1,484 lines 45.0 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} Changes the label position in the preview.
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 }
275 }
276
277 /**
278 * This function is used to update the form action when switching from the advanced settings and quick-settings.
279 *
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 }
330 }
331
332 /**
333 * When a style card is clicked, the preview is updated.
334 * If the Update button is clicked after selecting a style card, the active card will be saved as the target form's style.
335 *
336 * @param {Event} event
337 * @return {void}
338 */
339 function handleStyleCardClick( event ) {
340 const target = event.target;
341
342 if ( target.closest( '.dropdown' ) ) {
343 // Ignore the hamburger menu inside of the card.
344 return;
345 }
346
347 const card = target.classList.contains( 'frm-style-card' ) ? target : target.closest( '.frm-style-card' );
348 const cardIsLocked = card.classList.contains( 'frm-locked-style' );
349
350 if ( cardIsLocked ) {
351 maybeCreateStyleTemplateModal( card );
352 return; // Exit early as we're not actually selecting a locked template for preview.
353 }
354
355 const previewArea = document.getElementById( 'frm_style_preview' );
356 const activeCard = document.querySelector( '.frm-active-style-card' );
357 const sampleForm = document.getElementById( 'frm_sample_form' ).querySelector( '.frm_forms' );
358 const styleIdInput = getStyleIdInput();
359
360 disableLabelTransitions();
361
362 activeCard.classList.remove( 'frm-active-style-card' );
363 card.classList.add( 'frm-active-style-card' );
364
365 const form = previewArea.querySelector( 'form' );
366 if ( form ) {
367 // If you do not have a valid form selected, form may be null.
368 form.parentNode.classList.remove( activeCard.dataset.classname );
369 form.parentNode.classList.add( card.dataset.classname );
370 }
371
372 sampleForm.classList.remove( activeCard.dataset.classname );
373 sampleForm.classList.add( card.dataset.classname );
374
375 // cardIsLocked is always false here due to early return above.
376 styleIdInput.value = card.dataset.styleId;
377 trackListPageChange();
378
379 setTimeout( enableLabelTransitions, 1 );
380
381 // 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).
382 const editButton = document.getElementById( 'frm_edit_style' );
383 const showEditButton = null !== card.querySelector( '.frm-style-card-info' ); // Only the "Applied style" has card info.
384 editButton.classList.toggle( 'frm_hidden', ! showEditButton );
385
386 changeLabelPositionsInPreview( card.dataset.labelPosition );
387
388 // Trigger an action here so Pro can handle template preview updates on card click.
389 const hookName = 'frm_style_card_click';
390 const hookArgs = { card, styleIdInput };
391 wp.hooks.doAction( hookName, hookArgs );
392 }
393
394 /**
395 * @param {HTMLElement} card
396 * @return {HTMLElement} The modal element.
397 */
398 function maybeCreateStyleTemplateModal( card ) {
399 const titleElement = card.querySelector( '.frm-style-card-title' );
400 const templateTitle = titleElement.textContent;
401 const modal = maybeCreateModal(
402 'frm_style_template_modal',
403 {
404 content: getStyleTemplateModalContent( card ),
405 footer: getStyleTemplateModalFooter( card )
406 }
407 );
408 modal.querySelector( '.frm-modal-title' ).textContent = templateTitle;
409 return modal;
410 }
411
412 /**
413 * @param {HTMLElement} card
414 * @return {HTMLElement} The modal content element.
415 */
416 function getStyleTemplateModalContent( card ) {
417 const children = [];
418
419 children.push(
420 div( {
421 className: 'frm_warning_style',
422 children: [
423 span(
424 /* translators: %s: The required license type (ie. Plus, Business, or Elite) */
425 sprintf( __( 'Access to this style requires the %s plan.', 'formidable' ), card.dataset.requires )
426 ),
427 a( {
428 text: getUpgradeNowText(),
429 href: card.dataset.upgradeUrl,
430 target: '_blank'
431 } )
432 ]
433 } )
434 );
435
436 return div( { children } );
437 }
438
439 /**
440 * @param {HTMLElement} card
441 * @return {HTMLElement} The modal footer element.
442 */
443 function getStyleTemplateModalFooter( card ) {
444 const viewDemoSiteButton = footerButton( {
445 text: __( 'Learn More', 'formidable' ),
446 buttonType: 'secondary'
447 } );
448 viewDemoSiteButton.href = card.dataset.upgradeUrl;
449 viewDemoSiteButton.target = '_blank';
450
451 const primaryActionButton = footerButton( {
452 text: getUpgradeNowText(),
453 buttonType: 'primary'
454 } );
455
456 primaryActionButton.classList.remove( 'dismiss' );
457 primaryActionButton.setAttribute( 'href', card.dataset.upgradeUrl );
458 primaryActionButton.target = '_blank';
459
460 return div( {
461 children: [ viewDemoSiteButton, primaryActionButton ]
462 } );
463 }
464
465 /**
466 * @return {string} The upgrade now text.
467 */
468 function getUpgradeNowText() {
469 return __( 'Upgrade Now', 'formidable' );
470 }
471
472 /**
473 * Track an unsaved change on the edit page.
474 * This is included in the frmStylerFunctions global so unsaved changes can be tracked in Pro as well.
475 *
476 * @return {void}
477 */
478 function trackUnsavedChange() {
479 state.unsavedChanges = true;
480 }
481
482 /**
483 * Possibly prevent leaving the page if there are unsaved changes.
484 *
485 * @param {Event} event
486 * @return {void}
487 */
488 function maybeConfirmExit( event ) {
489 if ( ! state.unsavedChanges ) {
490 return;
491 }
492
493 event.preventDefault();
494 event.returnValue = '';
495 }
496
497 /**
498 * Floating labels have a transition style. Turn it off temporarily when switching between cards to avoid a transition between two different style classes.
499 *
500 * @return {void}
501 */
502 function disableLabelTransitions() {
503 setLabelTransitionStyle( 'none' );
504 }
505
506 /**
507 * @return {void}
508 */
509 function enableLabelTransitions() {
510 setLabelTransitionStyle( '' );
511 }
512
513 /**
514 * @param {string} value
515 * @return {void}
516 */
517 function setLabelTransitionStyle( value ) {
518 document.getElementById( 'frm_style_preview' ).querySelectorAll( '.frm_inside_container' ).forEach(
519 container => container.querySelector( 'label' ).style.transition = value
520 );
521 }
522
523 /**
524 * @return {void}
525 */
526 function toggleSampleForm() {
527 state.showingSampleForm = ! state.showingSampleForm;
528 document.getElementById( 'frm_active_style_form' ).classList.toggle( 'frm_hidden', state.showingSampleForm );
529 document.getElementById( 'frm_toggle_sample_form' ).querySelector( 'span' ).textContent = state.showingSampleForm ? __( 'View my form', 'formidable' ) : __( 'View sample form', 'formidable' );
530 }
531
532 /**
533 * @return {void}
534 */
535 function handleUpdateClick() {
536 state.unsavedChanges = false; // Prevent the saved changes pop up from triggering when submitting the form.
537
538 const form = document.getElementById( 'frm_styling_form' );
539 if ( form ) {
540 // Submitting for an "edit" view.
541 form.submit();
542 return;
543 }
544
545 document.getElementById( 'frm_submit_side_top' ).classList.add( 'frm_loading_button' );
546
547 // Submit the "list" view (assign a style to a form).
548 document.getElementById( 'frm_style_list_form' ).submit();
549 }
550
551 /**
552 * Maybe modify an anchor's URL on click.
553 * 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.
554 *
555 * @param {HTMLElement} clickTarget
556 * @return {void}
557 */
558 function modifyStylerUrl( clickTarget ) {
559 if ( ! state.showingSampleForm ) {
560 // Don't change the URL if it is not a sample form.
561 return;
562 }
563
564 const anchor = clickTarget.hasAttribute( 'href' ) ? clickTarget : clickTarget.querySelector( 'a[href]' );
565 anchor.setAttribute( 'href', anchor.getAttribute( 'href' ) + '&sample=1' );
566 }
567
568 /**
569 * Add menu dropdowns to style cards dynamically on load.
570 *
571 * @return {void}
572 */
573 function addHamburgerMenusToCards() {
574 const cards = Array.from( document.getElementsByClassName( 'frm-style-card' ) );
575 cards.forEach( card => maybeAddMenuToCard( card ) );
576 }
577
578 /**
579 * @param {HTMLElement} card
580 * @return {void}
581 */
582 function maybeAddMenuToCard( card ) {
583 if ( ! shouldAddMenuToCard( card ) ) {
584 return;
585 }
586
587 card.append( getHamburgerMenu( card.dataset ) );
588 }
589
590 /**
591 * Avoid adding a menu to an upsell card or a template card.
592 *
593 * @param {HTMLElement} card
594 * @return {boolean} Whether to add a menu to the card.
595 */
596 function shouldAddMenuToCard( card ) {
597 return 'frm_template_style_cards_wrapper' !== card.parentNode.id || ! card.classList.contains( 'frm-locked-style' );
598 }
599
600 /**
601 * @return {void}
602 */
603 function addHamburgerMenuForEditPage() {
604 const styleName = document.getElementById( 'frm_style_name' );
605 if ( ! styleName ) {
606 return;
607 }
608
609 const styleId = document.getElementById( 'frm_styling_form' ).querySelector( 'input[name="ID"]' ).value;
610
611 const hamburgerMenu = getHamburgerMenu( { styleId } );
612 hamburgerMenu.classList.add( 'alignright' );
613 styleName.parentNode.insertBefore( hamburgerMenu, styleName );
614 }
615
616 /**
617 * Get a dropdown and the "hamburger" stacked dot menu trigger for a single style card.
618 *
619 * @param {DOMStringMap} data {
620 * @type {string} editUrl
621 * @type {string} styleId
622 * @type {string} labelPosition
623 * @type {string} classname
624 * }
625 * @return {HTMLElement} The hamburger menu element.
626 */
627 function getHamburgerMenu( data ) {
628 const hamburgerMenu = a( {
629 className: 'frm-dropdown-toggle dropdown-toggle',
630 child: svg( { href: '#frm_thick_more_vert_icon' } )
631 } );
632 hamburgerMenu.setAttribute( 'data-bs-toggle', 'dropdown' );
633 hamburgerMenu.setAttribute( 'data-bs-container', 'body' );
634 hamburgerMenu.setAttribute( 'role', 'button' );
635 hamburgerMenu.setAttribute( 'tabindex', 0 );
636
637 const isTemplate = 'undefined' !== typeof data.templateKey;
638 let dropdownMenuOptions = [];
639
640 if ( isListPage ) {
641 const applyOption = a( {
642 text: isTemplate ? __( 'Install and apply', 'formidable' ) : __( 'Apply', 'formidable' )
643 } );
644 addIconToOption( applyOption, 'frm_save_icon' );
645 dropdownMenuOptions.push( { anchor: applyOption, type: 'apply' } );
646 onClickPreventDefault( applyOption, handleApplyOptionClick );
647 }
648
649 if ( ! isTemplate ) {
650 if ( 'string' === typeof data.editUrl ) {
651 // The Edit option is not included on the Edit page.
652 const editOption = a( {
653 text: __( 'Edit', 'formidable' ),
654 href: data.editUrl
655 } );
656 addIconToOption( editOption, 'frm_pencil_icon' );
657 dropdownMenuOptions.push( { anchor: editOption, type: 'edit' } );
658 }
659
660 const resetOption = a( {
661 text: __( 'Reset to Defaults', 'formidable' )
662 } );
663 addIconToOption( resetOption, 'frm_repeater_icon' );
664 onClickPreventDefault( resetOption, () => confirmResetStyle( data.styleId ) );
665
666 dropdownMenuOptions.push(
667 { anchor: getRenameOption( data.styleId ), type: 'rename' },
668 { anchor: resetOption, type: 'reset' }
669 );
670 }
671
672 const hookName = 'frm_style_card_dropdown_options';
673 const hookArgs = { data, addIconToOption, isTemplate };
674 dropdownMenuOptions = wp.hooks.applyFilters( hookName, dropdownMenuOptions, hookArgs );
675
676 if ( isListPage && ! isTemplate ) {
677 maybeAddDuplicateUpsell( dropdownMenuOptions );
678 }
679
680 const dropdownMenu = div( {
681 // Use dropdown-menu-right to avoid an overlapping issue with the card to the right (where the # of forms would appear above the menu).
682 className: 'frm-dropdown-menu frm-style-options-menu frm-p-1',
683 children: dropdownMenuOptions.map( wrapDropdownItem )
684 } );
685
686 const isRtl = document.body.classList.contains( 'rtl' );
687 dropdownMenu.classList.add( 'dropdown-menu-' + ( isRtl ? 'left' : 'right' ) );
688
689 dropdownMenu.setAttribute( 'role', 'menu' );
690
691 return div( {
692 className: 'dropdown frm_wrap', // The .frm_wrap class prevents a blue outline on the active dropdown trigger.
693 children: [ hamburgerMenu, dropdownMenu ]
694 } );
695 }
696
697 /**
698 * @param {Array} dropdownMenuOptions
699 * @return {void}
700 */
701 function maybeAddDuplicateUpsell( dropdownMenuOptions ) {
702 let duplicateOptionExists = false;
703 for ( let i = 0; i < dropdownMenuOptions.length; ++i ) {
704 if ( dropdownMenuOptions[ i ].type === 'duplicate' ) {
705 duplicateOptionExists = true;
706 break;
707 }
708 }
709
710 if ( duplicateOptionExists ) {
711 return;
712 }
713
714 const duplicateUpsell = a( {
715 text: __( 'Duplicate', 'formidable' ),
716 className: 'frm_noallow'
717 } );
718 addIconToOption( duplicateUpsell, 'frm_clone_icon' );
719 onClickPreventDefault( duplicateUpsell, () => document.getElementById( 'frm_new_style_trigger' ).click() );
720 const upsellOption = { anchor: duplicateUpsell, type: 'duplicate' };
721 dropdownMenuOptions.splice( 3, 0, upsellOption );
722 }
723
724 /**
725 * @param {Event} event
726 * @return {void}
727 */
728 function handleApplyOptionClick( event ) {
729 const option = event.target;
730 const card = option.closest( '.frm-style-card' );
731 if ( ! card ) {
732 return;
733 }
734
735 card.click();
736 handleUpdateClick();
737 }
738
739 /**
740 * @param {string} styleId
741 * @return {HTMLElement} The rename option element.
742 */
743 function getRenameOption( styleId ) {
744 const renameOption = a( __( 'Rename', 'formidable' ) );
745 addIconToOption( renameOption, 'frm_signature2_icon' );
746
747 let titleTarget;
748
749 // Depending on the page we're pulling the text from an existing element on the page.
750 if ( isListPage ) {
751 titleTarget = getCardByStyleId( styleId ).querySelector( '.frm-style-card-title' );
752 } else {
753 titleTarget = document.getElementById( 'frm_style_name' );
754 }
755
756 onClickPreventDefault(
757 renameOption,
758 () => {
759 const styleName = titleTarget.textContent;
760 stylerModal(
761 'frm_rename_style_modal',
762 {
763 title: __( 'Rename style', 'formidable' ),
764 content: getStyleInputNameModalContent( 'rename', styleName ),
765 footer: getRenameStyleModalFooter( styleId )
766 }
767 );
768 }
769 );
770
771 return renameOption;
772 }
773
774 /**
775 * @param {string} id
776 * @param {Object} args
777 * @return {HTMLElement} The modal element.
778 */
779 function stylerModal( id, args ) {
780 const modal = maybeCreateModal( id, args );
781 // Include both wp-core-ui and frm-white-body on the modal.
782 // Without wp-core-ui, the vertical alignment of the primary button is wrong.
783 // Without frm-white-body, cancel buttons in the modal do not get styled properly.
784 modal.classList.add( 'frm_common_modal', 'wp-core-ui', 'frm-white-body' );
785 return modal;
786 }
787
788 /**
789 * Get modal content with just a "Style Name" input.
790 * This is used for New style, Duplicate style, and for Rename style.
791 *
792 * @param {string} context
793 * @param {string|undefined} value
794 * @return {HTMLElement} The modal content element.
795 */
796 function getStyleInputNameModalContent( context, value ) {
797 // Create a form so we can listen to Enter key presses that trigger a form submit event.
798 const form = tag(
799 'form',
800 {
801 child: labelledTextInput( 'frm_' + context + '_style_name_input', __( 'Style name', 'formidable' ), 'style_name' )
802 }
803 );
804 form.addEventListener(
805 'submit',
806 /**
807 * @param {Event} event
808 * @return {false} Prevents the default action and stops the event from bubbling.
809 */
810 event => {
811 // Prevent the form in the modal from submitting and trigger the click button in the modal footer instead.
812 event.preventDefault();
813
814 const modal = form.closest( '.frm-dialog' );
815 modal.querySelector( '.frm_modal_footer .frm-button-primary' ).click();
816
817 return false;
818 }
819 );
820 const content = div( { child: form } );
821 content.style.padding = '20px';
822 content.querySelector( 'label' ).style.lineHeight = 1.5;
823
824 const styleNameInput = content.querySelector( 'input' );
825 styleNameInput.addEventListener(
826 'input',
827 () => {
828 const footerSubmitButton = styleNameInput.closest( '.frm_modal_content' ).nextElementSibling.querySelector( '.frm-button-primary' );
829 if ( '' === styleNameInput.value ) {
830 footerSubmitButton.setAttribute( 'disabled', 'disabled' );
831 footerSubmitButton.classList.remove( 'dismiss' );
832 } else {
833 footerSubmitButton.removeAttribute( 'disabled' );
834 footerSubmitButton.classList.add( 'dismiss' );
835 }
836 }
837 );
838
839 if ( 'string' === typeof value ) {
840 styleNameInput.value = value;
841 }
842
843 return content;
844 }
845
846 /**
847 * @param {string} styleId
848 * @return {HTMLElement} The modal footer element.
849 */
850 function getRenameStyleModalFooter( styleId ) {
851 const cancelButton = footerButton( { text: __( 'Cancel', 'formidable' ), buttonType: 'cancel' } );
852 cancelButton.classList.add( 'dismiss' );
853
854 const renameButton = footerButton( { text: __( 'Rename style', 'formidable' ), buttonType: 'primary' } );
855 onClickPreventDefault( renameButton, () => renameStyle( styleId ) );
856
857 return div( {
858 children: [ cancelButton, renameButton ]
859 } );
860 }
861
862 /**
863 * Call frm_rename_style action when the rename style button is clicked in rename modal.
864 *
865 * @param {string} styleId
866 * @return {void}
867 */
868 function renameStyle( styleId ) {
869 const styleNameInput = document.getElementById( 'frm_rename_style_name_input' );
870 const newStyleName = styleNameInput.value;
871
872 if ( '' === newStyleName ) {
873 // Avoid setting an empty name.
874 // The button gets disabled on an input event when the name is empty.
875 return;
876 }
877
878 const formData = new FormData();
879 formData.append( 'style_id', styleId );
880 formData.append( 'style_name', newStyleName );
881 doJsonPost( 'rename_style', formData ).then(
882 /**
883 * Sync the page with the new name of renamed style after successfully making a POST request.
884 *
885 * If on the list page, update the style card after renaming a style.
886 * On the edit page, update the style name element instead.
887 *
888 * @return {void}
889 */
890 () => {
891 success( __( 'Style has been renamed successfully', 'formidable' ) );
892
893 if ( isListPage ) {
894 updateStyleNameInCard( styleId, newStyleName );
895 return;
896 }
897
898 const titleSpan = document.getElementById( 'frm_style_name' );
899 titleSpan.textContent = newStyleName;
900 }
901 );
902 }
903
904 /**
905 * @param {string} styleId
906 * @param {string} newStyleName
907 * @return {void}
908 */
909 function updateStyleNameInCard( styleId, newStyleName ) {
910 const card = getCardByStyleId( styleId );
911 const titleElement = card.querySelector( '.frm-style-card-title' );
912 titleElement.textContent = newStyleName;
913 }
914
915 /**
916 * @param {string} styleId
917 * @return {HTMLElement} The card element.
918 */
919 function getCardByStyleId( styleId ) {
920 const defaultCard = document.querySelector( '#frm_default_style_cards_wrapper > div[data-style-id="' + styleId + '"]' );
921 if ( defaultCard ) {
922 return defaultCard;
923 }
924 return Array.from( document.getElementById( 'frm_custom_style_cards_wrapper' ).children ).find( card => card.dataset.styleId === styleId );
925 }
926
927 /**
928 * @param {HTMLElement} option
929 * @param {string} iconId
930 * @return {void}
931 */
932 function addIconToOption( option, iconId ) {
933 const icon = frmDom.svg( { href: '#' + iconId } );
934 option.insertBefore( icon, option.firstChild );
935 }
936
937 /**
938 * @param {string} styleId
939 * @return {void}
940 */
941 function confirmResetStyle( styleId ) {
942 stylerModal(
943 'frm_reset_style_modal',
944 {
945 title: __( 'Reset style', 'formidable' ),
946 content: getResetStyleModalContent(),
947 footer: getResetStyleModalFooter( styleId )
948 }
949 );
950 }
951
952 /**
953 * @return {HTMLElement} The modal content element.
954 */
955 function getResetStyleModalContent() {
956 const content = div( __( 'Reset this style back to the default?', 'formidable' ) );
957 content.style.padding = '20px';
958 return content;
959 }
960
961 /**
962 * @param {string} styleId
963 * @return {HTMLElement} The modal footer element.
964 */
965 function getResetStyleModalFooter( styleId ) {
966 const cancelButton = footerButton( {
967 text: __( 'Cancel', 'formidable' ),
968 buttonType: 'cancel'
969 } );
970 cancelButton.classList.add( 'dismiss' );
971 const resetButton = footerButton( {
972 text: __( 'Reset style', 'formidable' ),
973 buttonType: 'primary'
974 } );
975 onClickPreventDefault( resetButton, () => resetStyle( styleId ) );
976 return div( { children: [ cancelButton, resetButton ] } );
977 }
978
979 /**
980 * Handle reset dropdown action.
981 * This function handles the front end routing for the reset action as reset works differently for edit and list views.
982 *
983 * @param {string} styleId
984 * @return {void}
985 */
986 function resetStyle( styleId ) {
987 if ( isListPage ) {
988 resetStyleOnListPage( styleId );
989 return;
990 }
991 resetStyleOnEditPage();
992 }
993
994 /**
995 * Make a POST request to reset the style then reload the CSS and reset the card styles.
996 *
997 * @param {string} styleId
998 * @return {void}
999 */
1000 function resetStyleOnListPage( styleId ) {
1001 const formData = new FormData();
1002 formData.append( 'style_id', styleId );
1003 doJsonPost( 'settings_reset', formData ).then(
1004 response => {
1005 const card = getCardByStyleId( styleId );
1006 card.classList.remove( 'frm-dark-style' );
1007 if ( 'string' === typeof response.style ) {
1008 card.style = response.style;
1009 }
1010 reloadCSSAfterStyleReset();
1011 showStyleResetSuccessMessage();
1012 }
1013 );
1014 }
1015
1016 function showStyleResetSuccessMessage() {
1017 success( __( 'Style has been reset successfully', 'formidable' ) );
1018 }
1019
1020 /**
1021 * Reset the style in-page (without actually updating it).
1022 *
1023 * @return {void}
1024 */
1025 function resetStyleOnEditPage() {
1026 jQuery.ajax( {
1027 type: 'POST',
1028 url: ajaxurl,
1029 data: {
1030 action: 'frm_settings_reset',
1031 nonce: frmGlobal.nonce
1032 },
1033 success: syncEditPageAfterResetAction
1034 } );
1035 }
1036
1037 /**
1038 * Handle reset success on edit page.
1039 * This function sets all styling inputs to default values.
1040 *
1041 * @todo Stop triggering change events with jQuery. And remove the other jQuery as well.
1042 *
1043 * @param {Object} response
1044 * @return {void}
1045 */
1046 function syncEditPageAfterResetAction( response ) {
1047 let defaultValues = response.replace( /^\s+|\s+$/g, '' );
1048 if ( defaultValues.indexOf( '{' ) === 0 ) {
1049 defaultValues = JSON.parse( defaultValues );
1050 }
1051
1052 for ( const key in defaultValues ) {
1053 let targetInput = document.querySelector( 'input[name$="[' + key + ']"], select[name$="[' + key + ']"]' );
1054 if ( ! targetInput ) {
1055 continue;
1056 }
1057
1058 if ( 'radio' === targetInput.getAttribute( 'type' ) ) {
1059 // Reset the repeater icon dropdown.
1060 targetInput = document.querySelector( 'input[name$="[' + key + ']"][value="' + defaultValues[ key ] + '"]' );
1061 if ( targetInput ) {
1062 targetInput.checked = true;
1063 jQuery( targetInput ).trigger( 'change' );
1064 }
1065 continue;
1066 }
1067
1068 targetInput.value = defaultValues[ key ];
1069
1070 if ( targetInput.classList.contains( 'wp-color-picker' ) ) {
1071 // Trigger a change event so the color pickers sync. Otherwise they stay the same color after reset.
1072 jQuery( targetInput ).trigger( 'change' );
1073 }
1074 }
1075
1076 resetCustomCSSEditor();
1077 jQuery( '#frm_submit_style, #frm_auto_width' ).prop( 'checked', false );
1078 jQuery( document.getElementById( 'frm_fieldset' ) ).trigger( 'change' );
1079 showStyleResetSuccessMessage();
1080 }
1081
1082 /**
1083 * Reset the custom CSS editor.
1084 *
1085 * @return {void}
1086 */
1087 function resetCustomCSSEditor() {
1088 const checkbox = document.getElementById( 'frm_enable_single_style_custom_css' );
1089 const editorWrapper = document.getElementById( 'frm_single_style_custom_css_editor' );
1090 if ( ! checkbox || ! editorWrapper ) {
1091 return;
1092 }
1093 checkbox.checked = false;
1094 editorWrapper.classList.add( 'frm_hidden' );
1095 }
1096
1097 /**
1098 * Reload Formidable CSS after a style is reset so the preview updates immediately without needing to reload the page.
1099 *
1100 * @return {void}
1101 */
1102 function reloadCSSAfterStyleReset() {
1103 const style = document.getElementById( 'frm-custom-theme-css' );
1104 if ( ! style ) {
1105 return;
1106 }
1107
1108 const newStyle = document.createElement( 'link' );
1109 newStyle.rel = 'stylesheet';
1110 newStyle.type = 'text/css';
1111 newStyle.href = style.href + '&key=' + getAutoId(); // Make the URL unique so the old stylesheet doesn't get picked up by cache.
1112
1113 // Listen for the new style to load before removing the old style to avoid having no styles while the new style is loading.
1114 newStyle.addEventListener(
1115 'load',
1116 () => {
1117 style.remove();
1118 newStyle.id = 'frm-custom-theme-css'; // Assign the old ID to the new style so it can be removed in the next reset action.
1119 }
1120 );
1121
1122 const head = document.getElementsByTagName( 'HEAD' )[ 0 ];
1123 head.append( newStyle );
1124 }
1125
1126 /**
1127 * @return {number} The auto ID.
1128 */
1129 function getAutoId() {
1130 return ++state.autoId;
1131 }
1132
1133 /**
1134 * @param {Object} data
1135 * @param {HTMLElement} data.anchor
1136 * @param {string} data.type
1137 * @return {HTMLElement} The dropdown item element.
1138 */
1139 function wrapDropdownItem( { anchor, type } ) {
1140 return div( {
1141 className: 'dropdown-item frm-' + type + '-style',
1142 child: anchor
1143 } );
1144 }
1145
1146 /**
1147 * This gets triggered through a hook called in frmAdminBuild.styleInit() from formidable_admin.js.
1148 *
1149 * @return {void}
1150 */
1151 function initEditPage() {
1152 const { debounce } = frmDom.util;
1153 const debouncedPreviewUpdate = debounce( () => changeStyling(), 100 );
1154 const debouncedColorChange = debounce( ( event, value ) => {
1155 /**
1156 * Fires on style colorpicker change.
1157 *
1158 * @param {Event} data.event The color change event.
1159 * @param {string} data.value New color value.
1160 */
1161 wp.hooks.doAction( 'frm_style_options_color_change', { event, value } );
1162 }, 200 );
1163
1164 const debouncedTextSquishCheck = debounce( textSquishCheck, 300 );
1165 initPosClass(); // It's important that this gets called before we add event listeners because it triggers change events.
1166
1167 [ 'frm_field_height', 'frm_field_font_size', 'frm_field_pad' ].forEach( selector => {
1168 document.getElementById( selector ).addEventListener( 'change', debouncedTextSquishCheck );
1169 } );
1170
1171 jQuery( 'input.hex' ).wpColorPicker( {
1172 change: function( event, ui ) {
1173 let color = jQuery( this ).wpColorPicker( 'color' );
1174 trackUnsavedChange();
1175 if ( ui.color._alpha < 1 ) {
1176 // If there's transparency, use RGBA
1177 color = ui.color.toCSS( 'rgba' );
1178 }
1179 debouncedColorChange( event, color );
1180
1181 if ( null !== event.target.getAttribute( 'data-alpha-color-type' ) ) {
1182 debouncedPreviewUpdate();
1183 return;
1184 }
1185
1186 jQuery( event.target ).val( color ).trigger( 'change' );
1187 }
1188 } );
1189 jQuery( '.wp-color-result-text' ).text( function( _, oldText ) {
1190 const container = jQuery( this ).closest( '.wp-picker-container' );
1191 if ( 'undefined' !== typeof container && container[ 0 ].parentElement.classList.contains( 'frm-colorpicker' ) ) {
1192 return container[ 0 ].querySelector( '.wp-color-picker' ).value;
1193 }
1194 return oldText === 'Select Color' ? 'Select' : oldText;
1195 } );
1196 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 );
1197
1198 // 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.
1199 initDatepickerSample();
1200
1201 addHamburgerMenuForEditPage();
1202
1203 document.getElementById( 'frm_styling_form' ).querySelectorAll( 'input, select' ).forEach(
1204 input => input.addEventListener( 'change', () => trackUnsavedChange() )
1205 );
1206
1207 /**
1208 * Sends an AJAX POST request for new CSS to use for the preview.
1209 * This is called whenever a style setting is changed, generally using debouncedPreviewUpdate to avoid simultaneous requests.
1210 *
1211 * @return {void}
1212 */
1213 function changeStyling() {
1214 const styleInputs = Array.from( document.getElementById( 'frm_style_sidebar' ).querySelectorAll( 'input, select, textarea' ) ).filter(
1215 input => 'style_name' === input.name || 0 === input.name.indexOf( 'frm_style_setting[post_content]' )
1216 );
1217 const locStr = JSON.stringify( jQuery( styleInputs ).serializeArray() );
1218
1219 jQuery.ajax( {
1220 type: 'POST',
1221 url: ajaxurl,
1222 data: {
1223 action: 'frm_change_styling',
1224 nonce: frmGlobal.nonce,
1225 frm_style_setting: locStr
1226 },
1227 success: css => {
1228 handleChangeStylingSuccess( css );
1229 setSelectPlaceholderColor();
1230 }
1231 } );
1232 }
1233
1234 /**
1235 * Update the CSS used for the preview on the edit page when a styling input has been updated.
1236 *
1237 * @param {string} css The response from the frm_change_styling request.
1238 * @return {void}
1239 */
1240 function handleChangeStylingSuccess( css ) {
1241 // Validate the string response. A valid output will include rules with .with_frm_style
1242 if ( -1 === css.indexOf( '.with_frm_style' ) ) {
1243 // Handle error (possibly a permission error, or an outdated nonce).
1244 alert( css );
1245 return;
1246 }
1247 document.getElementById( 'this_css' ).innerHTML = css;
1248 }
1249
1250 /**
1251 * 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".
1252 * This can be triggered when modifying font size, height, and padding.
1253 *
1254 * @return {void}
1255 */
1256 function textSquishCheck() {
1257 if ( null !== frmDom.util.getCookie( 'frm-style-text-squish-check' ) ) {
1258 return;
1259 }
1260 const height = document.getElementById( 'frm_field_height' ).value.replace( /\D/g, '' );
1261 const paddingEntered = document.getElementById( 'frm_field_pad' ).value.split( ' ' );
1262 const paddingCount = paddingEntered.length;
1263
1264 frmDom.util.setCookie( 'frm-style-text-squish-check', 1, 30 );
1265
1266 // If too many or too few padding entries, leave now
1267 if ( paddingCount === 0 || paddingCount > 4 || height === '' ) {
1268 return;
1269 }
1270
1271 const size = document.getElementById( 'frm_field_font_size' ).value.replace( /\D/g, '' );
1272 // Get the top and bottom padding from entered values
1273 const paddingTop = paddingEntered[ 0 ].replace( /\D/g, '' );
1274 let paddingBottom = paddingTop;
1275 if ( paddingCount >= 3 ) {
1276 paddingBottom = paddingEntered[ 2 ].replace( /\D/g, '' );
1277 }
1278
1279 // Check if there is enough space for text
1280 const textSpace = height - size - paddingTop - paddingBottom - 3;
1281 if ( textSpace < 0 ) {
1282 frmAdminBuild.infoModal( frm_admin_js.css_invalid_size );
1283 }
1284 }
1285
1286 /**
1287 * When the Collapse icons are updated, sync the dropdown.
1288 * Otherwise the previously selected value will still appear as the selected value.
1289 *
1290 * @return {void}
1291 */
1292 jQuery( document ).on( 'change', '.frm-dropdown-menu input[type="radio"]', function() {
1293 trackUnsavedChange();
1294
1295 const radio = this;
1296 const btnGrp = radio.closest( '.btn-group' );
1297 const btnId = btnGrp.getAttribute( 'id' );
1298
1299 const select = document.getElementById( btnId.replace( '_select', '' ) );
1300 if ( select ) {
1301 select.value = radio.value;
1302 }
1303
1304 jQuery( btnGrp ).children( 'button' ).html( radio.nextElementSibling.innerHTML + ' <b class="caret"></b>' );
1305
1306 const activeItem = btnGrp.querySelector( '.dropdown-item.active' );
1307 if ( activeItem ) {
1308 activeItem.classList.remove( 'active' );
1309 }
1310
1311 radio.closest( '.dropdown-item' ).classList.add( 'active' );
1312 } );
1313
1314 if ( frm_admin_js.requireAccordionTitleClickListener ) {
1315 document.querySelectorAll( '.styling_settings h3.accordion-section-title' ).forEach( el => {
1316 el.addEventListener( 'click', event => {
1317 if ( ! event.target.closest( 'button' ) ) {
1318 el.querySelector( 'button' ).click();
1319 }
1320 } );
1321 } );
1322 }
1323 }
1324
1325 /**
1326 * @param {HTMLElement} input
1327 * @param {HTMLElement} container
1328 * @return {void}
1329 */
1330 function checkFloatingLabelsForStyles( input, container ) {
1331 if ( ! container ) {
1332 container = input.closest( '.frm_inside_container' );
1333 }
1334
1335 const shouldFloatTop = input.value || document.activeElement === input;
1336
1337 container.classList.toggle( 'frm_label_float_top', shouldFloatTop );
1338
1339 if ( 'SELECT' !== input.tagName ) {
1340 return;
1341 }
1342
1343 const firstOpt = input.querySelector( 'option:first-child' );
1344
1345 if ( shouldFloatTop ) {
1346 if ( firstOpt.hasAttribute( 'data-label' ) ) {
1347 firstOpt.textContent = firstOpt.getAttribute( 'data-label' );
1348 firstOpt.removeAttribute( 'data-label' );
1349 }
1350 } else if ( firstOpt.textContent ) {
1351 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1352 firstOpt.textContent = '';
1353 }
1354 }
1355
1356 /**
1357 * @return {void}
1358 */
1359 function initPosClass() {
1360 const positionSetting = document.getElementById( 'frm_position' );
1361
1362 jQuery( positionSetting ).on( 'change', setPosClass );
1363
1364 // Trigger label position option on load.
1365 const changeEvent = document.createEvent( 'HTMLEvents' );
1366 changeEvent.initEvent( 'change', true, false );
1367 positionSetting.dispatchEvent( changeEvent );
1368 }
1369
1370 /**
1371 * Update label container classes when the label "Position" setting is changed.
1372 *
1373 * @return {void}
1374 */
1375 function setPosClass() {
1376 /*jshint validthis:true */
1377 let value = this.value;
1378 if ( value === 'none' ) {
1379 value = 'top';
1380 } else if ( value === 'no_label' ) {
1381 value = 'none';
1382 }
1383
1384 document.getElementById( 'frm_style_preview' ).querySelectorAll( '.frm_form_field.frm-default-label-position, #frm_sample_form .frm_form_field' ).forEach( container => {
1385 const input = container.querySelector( ':scope > input, :scope > select, :scope > textarea' ); // Fields that support floating label should have a directly child input/textarea/select.
1386 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.
1387 const currentValue = shouldForceTopStyling ? 'top' : value;
1388
1389 container.classList.remove( 'frm_top_container', 'frm_left_container', 'frm_right_container', 'frm_none_container', 'frm_inside_container' );
1390 container.classList.add( 'frm_' + currentValue + '_container' );
1391
1392 if ( 'inside' === currentValue ) {
1393 checkFloatingLabelsForStyles( input, container );
1394 }
1395 } );
1396 }
1397
1398 /**
1399 * @return {void}
1400 */
1401 function initFloatingLabels() {
1402 [ 'focus', 'blur', 'change' ].forEach(
1403 eventName => frmDom.util.documentOn(
1404 eventName,
1405 '#frm_style_preview .frm_inside_container > input, #frm_style_preview .frm_inside_container > textarea, #frm_style_preview .frm_inside_container > select',
1406 event => checkFloatingLabelsForStyles( event.target ),
1407 true
1408 )
1409 );
1410 }
1411
1412 /**
1413 * The signature add on expects that validateFormSubmit is callable.
1414 * Without this, drawing in a signature field triggers a "Uncaught ReferenceError: frmFrontForm is not defined" error.
1415 * We don't want the validation to actually triggr, so just fill in an empty function.
1416 *
1417 * @return {void}
1418 */
1419 function fillMissingSignatureValidationFunction() {
1420 if ( 'undefined' === typeof window.__FRMSIG || 'undefined' !== typeof window.frmFrontForm ) {
1421 return;
1422 }
1423
1424 window.frmFrontForm = { validateFormSubmit: () => {} };
1425 }
1426
1427 /**
1428 * Enable the datepicker in the sample form preview.
1429 *
1430 * @return {void}
1431 */
1432 function initDatepickerSample() {
1433 // If flatpickr is defined, then is a recent version of Pro which handles the datepicker preview as it's a PRO feature.
1434 if ( 'undefined' !== typeof flatpickr ) {
1435 return;
1436 }
1437
1438 const $sample = jQuery( '#datepicker_sample' );
1439 if ( $sample.length && 'function' === typeof $sample.datepicker ) {
1440 $sample.datepicker( { changeMonth: true, changeYear: true } );
1441 }
1442 }
1443
1444 /**
1445 * Set color for select placeholders.
1446 *
1447 * @since 6.5.1
1448 */
1449 function setSelectPlaceholderColor() {
1450 const selects = document.querySelectorAll( '.form-field select' );
1451 const styleElement = document.querySelector( '.with_frm_style' );
1452 const textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : '';
1453
1454 // Exit if there are no select elements or the textColorDisabled property is missing
1455 if ( ! selects.length || ! textColorDisabled ) {
1456 return;
1457 }
1458
1459 // Function to change the color of a select element
1460 const changeSelectColor = select => {
1461 if ( select.options[ select.selectedIndex ] && select.options[ select.selectedIndex ].classList.contains( 'frm-select-placeholder' ) ) {
1462 select.style.setProperty( 'color', textColorDisabled, 'important' );
1463 } else {
1464 select.style.color = '';
1465 }
1466 };
1467
1468 // Use a loop to iterate through each select element
1469 selects.forEach( select => {
1470 // Apply the color change to each select element
1471 changeSelectColor( select );
1472
1473 // Add an event listener for future changes
1474 select.addEventListener( 'change', () => changeSelectColor( select ) );
1475 } );
1476 }
1477
1478 // Hook into the styleInit function in formidable_admin.js
1479 wp.hooks.addAction( 'frm_style_editor_init', 'formidable', initEditPage );
1480
1481 // Set a global object so these functions can be re-used in Pro.
1482 window.frmStylerFunctions = { getCardByStyleId, getStyleInputNameModalContent, trackUnsavedChange, stylerModal };
1483 }() );
1484