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

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

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