PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.14
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.14
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.14, 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 const { __ } = wp.i18n;
10 const state = {
11 showingSampleForm: document.getElementById( 'frm_active_style_form' ).classList.contains( 'frm_hidden' ), // boolean
12 unsavedChanges: false, // boolean
13 autoId: 0, // Number
14 // Track the value of the selected style ID on page (on the list page).
15 // This is tracked to determine if there are actually unsaved changes.
16 // This way when you switch back to the initial value it doesn't count as a change.
17 initialSelectedStyleValue: false // String|false
18 };
19 const { div, span, a, labelledTextInput, tag, svg, success } = frmDom;
20 const { onClickPreventDefault } = frmDom.util;
21 const { maybeCreateModal, footerButton } = frmDom.modal;
22 const { doJsonPost } = frmDom.ajax;
23
24 const isListPage = document.getElementsByClassName( 'frm-style-card' ).length > 0;
25 if ( isListPage ) {
26 initListPage();
27 }
28
29 initCommonEventListeners();
30 initPreview();
31 fixWpAuthModal();
32
33 /**
34 * These are shared events for both the edit/list views like the sample form toggle.
35 * This includes preview events, but also the update button click event handling for both views.
36 *
37 * @returns {void}
38 */
39 function initCommonEventListeners() {
40 document.addEventListener( 'click', handleCommonClickEvents );
41 window.addEventListener( 'beforeunload', maybeConfirmExit );
42 disablePreviewSubmitButtons();
43 }
44
45 /**
46 * Initialize common functions required for the preview in both the edit and list views.
47 *
48 * @returns {void}
49 */
50 function initPreview() {
51 initFloatingLabels();
52 fillMissingSignatureValidationFunction();
53 setSelectPlaceholderColor();
54
55 // Remove .wp-core-ui from the body so the preview can avoid it.
56 // Then add it back where we want to use admin styles (the sidebar, otherwise inputs appear short).
57 document.body.classList.remove( 'wp-core-ui' );
58 document.getElementById( 'frm_style_sidebar' ).classList.add( 'wp-core-ui' );
59
60 jQuery( document ).on( 'input change', 'input[data-frmrange]', initSliderPreview );
61
62 }
63
64 /**
65 * Initialize the slider functionality in the style preview.
66 *
67 * @param {HTMLElement} event
68 * @returns {void}
69 */
70 function initSliderPreview( event ) {
71 const wrapper = event.target.closest( '.frm_range_container' );
72 if ( null === wrapper ) {
73 return;
74 }
75 wrapper.querySelector( '.frm_range_value' ).innerHTML = parseInt( this.value, 10 );
76 }
77
78 /**
79 * Add the wp-core-ui class to the #wp-auth-check-wrap element.
80 * As this style isn't included on the body for the styler, the close button on the auth modal wasn't getting styled properly.
81 *
82 * @returns {void}
83 */
84 function fixWpAuthModal() {
85 const authWrap = document.getElementById( 'wp-auth-check-wrap' );
86 if ( authWrap ) {
87 authWrap.classList.add( 'wp-core-ui' );
88 }
89 }
90
91 /**
92 * @returns {void}
93 */
94 function initListPage() {
95 document.addEventListener( 'click', handleClickEventsForListPage );
96 // Add a timeout so Pro has a chance to add a filter first.
97 // 0 does not always work in Google Chrome, so use 1.
98 setTimeout( addHamburgerMenusToCards, 1 );
99 initDatepickerSample();
100
101 const enableToggle = document.getElementById( 'frm_enable_styling' );
102 const styleIdInput = getStyleIdInput();
103 state.initialSelectedStyleValue = styleIdInput.value;
104
105 enableToggle.addEventListener( 'change', handleEnableStylingToggleChange );
106
107 syncPreviewFormLabelPositionsWithActiveStyle();
108 initStyleCardPagination();
109 }
110
111 /**
112 * Update label position in preview on list page.
113 * On the edit page this is handled with the initPosClass function instead.
114 */
115 function syncPreviewFormLabelPositionsWithActiveStyle() {
116 const activeCard = getActiveCard();
117 if ( activeCard ) {
118 changeLabelPositionsInPreview( activeCard.dataset.labelPosition );
119 }
120 }
121
122 /**
123 * Handle pagination click events.
124 *
125 * @returns {void}
126 */
127 function initStyleCardPagination() {
128 document.querySelectorAll( '.frm-style-card-pagination' ).forEach(
129 pagination => {
130 const wrapper = pagination.closest( '.frm-style-card-wrapper' );
131 const showAllAnchor = pagination.querySelector( '.frm-show-all-styles' );
132 let showingAll = false;
133
134 onClickPreventDefault(
135 showAllAnchor,
136 () => {
137 showingAll = ! showingAll;
138
139 if ( showingAll ) {
140 wrapper.querySelectorAll( '.frm-style-card' ).forEach(
141 card => card.classList.remove( 'frm_hidden' )
142 );
143 showAllAnchor.textContent = __( 'Show less', 'formidable' );
144 return;
145 }
146
147 wrapper.querySelectorAll( '.frm-style-card:nth-child(3) ~ .frm-style-card' ).forEach(
148 card => card.classList.add( 'frm_hidden' )
149 );
150 const hiddenCount = wrapper.querySelectorAll( '.frm-style-card.frm_hidden' ).length;
151 showAllAnchor.textContent = __( 'Show all (%d)', 'formidable' ).replace( '%d', hiddenCount );
152 }
153 );
154 }
155 );
156 }
157
158 /**
159 * @param {String} labelPosition
160 * @returns {void}
161 */
162 function changeLabelPositionsInPreview( labelPosition ) {
163 const input = tag( 'input' );
164 input.value = labelPosition;
165 setPosClass.bind( input )();
166 }
167
168 /**
169 * @returns {HTMLElement}
170 */
171 function getActiveCard() {
172 return document.querySelector( '.frm-active-style-card' );
173 }
174
175 /**
176 * When Formidable styling is disabled, the list of styles fades out.
177 * The style ID value associated with the selected style card gets cleared.
178 * This is because disabling styles is linked to the custom_style option as well.
179 *
180 * @param {Event} event
181 * @returns {void}
182 */
183 function handleEnableStylingToggleChange( event ) {
184 const stylesEnabled = event.target.checked;
185
186 document.querySelectorAll( '.frm-style-card-wrapper' ).forEach(
187 cardWrapper => cardWrapper.classList.toggle( 'frm-styles-enabled', stylesEnabled )
188 );
189
190 if ( ! stylesEnabled ) {
191 const styleIdInput = getStyleIdInput();
192 styleIdInput.value = '0';
193 trackListPageChange();
194 toggleFormidableStylingInPreviewForms( false );
195 return;
196 }
197
198 toggleFormidableStylingInPreviewForms( true );
199
200 // Click the active card so the style id input properly syncs.
201 // In Pro, templates use a templateKey attribute so we don't always want card.dataset.styleId
202 // There is no need to call trackListPageChange as it happens in the click event.
203 const card = document.querySelector( '.frm-active-style-card' );
204 if ( card ) {
205 card.click();
206 }
207 }
208
209 /**
210 * Track unsaved changes on the list page.
211 * All settings on the list page are mapped to the value of the styleIdInput.
212 * We track the value on load with state.initialSelectedStyleValue.
213 * Only consider unsaved changes on the page when this variable is no longer set to the original value.
214 *
215 * @returns {void}
216 */
217 function trackListPageChange() {
218 const styleIdInput = getStyleIdInput();
219 state.unsavedChanges = styleIdInput.value !== state.initialSelectedStyleValue;
220 }
221
222 /**
223 * @param {boolean} on
224 * @returns {void}
225 */
226 function toggleFormidableStylingInPreviewForms( on ) {
227 const preview = document.getElementById( 'frm_style_preview' );
228 const activeCard = getActiveCard();
229
230 let selector = '.frm_forms';
231 if ( ! on ) {
232 selector += '.with_frm_style';
233 }
234
235 preview.querySelectorAll( selector ).forEach(
236 formParent => {
237 formParent.classList.toggle( 'with_frm_style', on );
238 formParent.classList.toggle( activeCard.dataset.classname, on );
239 }
240 );
241 }
242
243 /**
244 * @returns {HTMLElement}
245 */
246 function getStyleIdInput() {
247 return document.getElementById( 'frm_style_list_form' ).querySelector( '[name="style_id"]' );
248 }
249
250 /**
251 * @param {Event} event
252 * @returns {void}
253 */
254 function handleCommonClickEvents( event ) {
255 const target = event.target;
256
257 if ( 'frm_toggle_sample_form' === target.id || target.closest( '#frm_toggle_sample_form' ) ) {
258 toggleSampleForm();
259 return;
260 }
261
262 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' ) ) {
263 switchAdvancedSettingsFormAction( target );
264 handleUpdateClick();
265 return;
266 }
267
268 if ( target.classList.contains( 'frm-edit-style' ) || null !== target.closest( '.frm-edit-style' ) || 'frm_edit_style' === target.id ) {
269 modifyStylerUrl( target );
270 return;
271 }
272 }
273
274 /**
275 * This function is used to update the form action when switching from the advanced settings and quick-settings.
276 * @param {Object} target The submit button event target
277 * @return {void}
278 */
279 function switchAdvancedSettingsFormAction( target ) {
280 const form = document.querySelector( '#frm_styling_form' );
281 if ( null === form ) {
282 return;
283 }
284 if ( target.closest( 'a#frm_style_back_to_quick_settings' ) ) {
285 form.action = form.action.replace( '&section=advanced-settings', '' );
286 return;
287 }
288 if ( 'frm-style-advanced-settings-button' === target.id ) {
289 form.action += '&section=advanced-settings';
290 }
291 }
292
293 /**
294 * @returns {void}
295 */
296 function disablePreviewSubmitButtons() {
297 const preview = document.getElementById( 'frm_style_preview' );
298 preview.querySelectorAll( 'form' ).forEach(
299 form => form.addEventListener(
300 'submit',
301 /**
302 * Prevent form submit event.
303 *
304 * @param {Event} event
305 * @returns {false}
306 */
307 event => {
308 event.preventDefault();
309 event.stopPropagation();
310 return false;
311 }
312 )
313 );
314 }
315
316 /**
317 * @param {Event} event
318 * @returns {void}
319 */
320 function handleClickEventsForListPage( event ) {
321 const target = event.target;
322
323 if ( target.classList.contains( 'frm-style-card' ) || target.closest( '.frm-style-card' ) ) {
324 handleStyleCardClick( event );
325 return;
326 }
327 }
328
329 /**
330 * When a style card is clicked, the preview is updated.
331 * If the Update button is clicked after selecting a style card, the active card will be saved as the target form's style.
332 *
333 * @param {Event} event
334 * @returns {void}
335 */
336 function handleStyleCardClick( event ) {
337 const target = event.target;
338
339 if ( target.closest( '.dropdown' ) ) {
340 // Ignore the hamburger menu inside of the card.
341 return;
342 }
343
344 const card = target.classList.contains( 'frm-style-card' ) ? target : target.closest( '.frm-style-card' );
345 const cardIsLocked = card.classList.contains( 'frm-locked-style' );
346
347 if ( cardIsLocked ) {
348 maybeCreateStyleTemplateModal( card );
349 return; // Exit early as we're not actually selecting a locked template for preview.
350 }
351
352 const previewArea = document.getElementById( 'frm_style_preview' );
353 const activeCard = document.querySelector( '.frm-active-style-card' );
354 const sampleForm = document.getElementById( 'frm_sample_form' ).querySelector( '.frm_forms' );
355 const styleIdInput = getStyleIdInput();
356
357 disableLabelTransitions();
358
359 activeCard.classList.remove( 'frm-active-style-card' );
360 card.classList.add( 'frm-active-style-card' );
361
362 const form = previewArea.querySelector( 'form' );
363 if ( form ) {
364 // If you do not have a valid form selected, form may be null.
365 form.parentNode.classList.remove( activeCard.dataset.classname );
366 form.parentNode.classList.add( card.dataset.classname );
367 }
368
369 sampleForm.classList.remove( activeCard.dataset.classname );
370 sampleForm.classList.add( card.dataset.classname );
371
372 if ( ! cardIsLocked ) {
373 // Don't update the form when a locked card is clicked.
374 styleIdInput.value = card.dataset.styleId;
375 trackListPageChange();
376 }
377
378 setTimeout( enableLabelTransitions, 1 );
379
380 // 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).
381 const editButton = document.getElementById( 'frm_edit_style' );
382 const showEditButton = null !== card.querySelector( '.frm-style-card-info' ); // Only the "Applied style" has card info.
383 editButton.classList.toggle( 'frm_hidden', ! showEditButton );
384
385 changeLabelPositionsInPreview( card.dataset.labelPosition );
386
387 // Trigger an action here so Pro can handle template preview updates on card click.
388 const hookName = 'frm_style_card_click';
389 const hookArgs = { card, styleIdInput };
390 wp.hooks.doAction( hookName, hookArgs );
391 }
392
393 /**
394 * @param {HTMLElement} card
395 * @returns {HTMLElement}
396 */
397 function maybeCreateStyleTemplateModal( card ) {
398 const titleElement = card.querySelector( '.frm-style-card-title' );
399 const templateTitle = titleElement.textContent;
400 const modal = maybeCreateModal(
401 'frm_style_template_modal',
402 {
403 content: getStyleTemplateModalContent( card ),
404 footer: getStyleTemplateModalFooter( card )
405 }
406 );
407 modal.querySelector( '.frm-modal-title' ).textContent = templateTitle;
408 return modal;
409 }
410
411 /**
412 * @param {HTMLElement} card
413 * @returns {HTMLElement}
414 */
415 function getStyleTemplateModalContent( card ) {
416 const children = [];
417
418 children.push(
419 div({
420 className: 'frm_warning_style',
421 children: [
422 span(
423 /* translators: %s: The required license type (ie. Plus, Business, or Elite) */
424 __( 'Access to this style requires the %s plan.', 'formidable' )
425 .replace( '%s', 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 * @returns {HTMLElement}
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 let 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 * @returns {String}
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 * @returns {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 * @returns {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 * @returns {void}
501 */
502 function disableLabelTransitions() {
503 setLabelTransitionStyle( 'none' );
504 }
505
506 /**
507 * @returns {void}
508 */
509 function enableLabelTransitions() {
510 setLabelTransitionStyle( '' );
511 }
512
513 /**
514 * @param {String} value
515 * @returns {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 * @returns {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 * @returns {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 * @returns {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 * @returns {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 {HTMLlement} card
580 * @returns {void}
581 */
582 function maybeAddMenuToCard( card ) {
583 if ( ! shouldAddMenuToCard( card ) ) {
584 return;
585 }
586
587 card.appendChild( getHamburgerMenu( card.dataset ) );
588 }
589
590 /**
591 * Avoid adding a menu to an upsell card or a template card.
592 *
593 * @param {HTMLlement} card
594 * @returns {boolean}
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 * @returns {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 * @returns {HTMLElement}
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-toggle', 'dropdown' );
633 hamburgerMenu.setAttribute( 'data-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 * @returns {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 * @returns {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 * @returns {HTMLElement}
742 */
743 function getRenameOption( styleId ) {
744 const renameOption = a( __( 'Rename', 'formidable' ) );
745 addIconToOption( renameOption, 'frm_signature_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 * @returns {HTMLElement}
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 * @returns {HTMLElement}
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 * @returns {false}
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 * @returns {HTMLELement}
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 * @returns {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 * @returns {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 * @returns {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} templateKey
917 * @returns {HTMLElement}
918 */
919 function getTemplateCard( templateKey ) {
920 const templateCard = document.getElementById( 'frm_template_style_cards_wrapper' ).querySelector( '.frm-style-card[data-template-key="' + templateKey + '"]' );
921 return templateCard;
922 }
923
924 /**
925 * @param {String} styleId
926 * @returns {HTMLElement}
927 */
928 function getCardByStyleId( styleId ) {
929 const defaultCard = document.querySelector( '#frm_default_style_cards_wrapper > div[data-style-id="' + styleId + '"]' );
930 if ( defaultCard ) {
931 return defaultCard;
932 }
933 return Array.from( document.getElementById( 'frm_custom_style_cards_wrapper' ).children ).find( card => card.dataset.styleId === styleId );
934 }
935
936 /**
937 * @param {HTMLElement} option
938 * @param {String} iconId
939 * @returns {void}
940 */
941 function addIconToOption( option, iconId ) {
942 const icon = frmDom.svg({ href: '#' + iconId });
943 option.insertBefore( icon, option.firstChild );
944 }
945
946 /**
947 * @param {String} styleId
948 * @returns {void}
949 */
950 function confirmResetStyle( styleId ) {
951 stylerModal(
952 'frm_reset_style_modal',
953 {
954 title: __( 'Reset style', 'formidable' ),
955 content: getResetStyleModalContent(),
956 footer: getResetStyleModalFooter( styleId )
957 }
958 );
959 }
960
961 /**
962 * @returns {HTMLElement}
963 */
964 function getResetStyleModalContent() {
965 const content = div( __( 'Reset this style back to the default?', 'formidable' ) );
966 content.style.padding = '20px';
967 return content;
968 }
969
970 /**
971 * @param {String} styleId
972 * @returns {HTMLElement}
973 */
974 function getResetStyleModalFooter( styleId ) {
975 const cancelButton = footerButton({
976 text: __( 'Cancel', 'formidable' ),
977 buttonType: 'cancel'
978 });
979 cancelButton.classList.add( 'dismiss' );
980 const resetButton = footerButton({
981 text: __( 'Reset style', 'formidable' ),
982 buttonType: 'primary'
983 });
984 onClickPreventDefault( resetButton, () => resetStyle( styleId ) );
985 return div({ children: [ cancelButton, resetButton ] });
986 }
987
988 /**
989 * Handle reset dropdown action.
990 * This function handles the front end routing for the reset action as reset works differently for edit and list views.
991 *
992 * @param {String} styleId
993 * @returns {void}
994 */
995 function resetStyle( styleId ) {
996 if ( isListPage ) {
997 resetStyleOnListPage( styleId );
998 return;
999 }
1000 resetStyleOnEditPage();
1001 }
1002
1003 /**
1004 * Make a POST request to reset the style then reload the CSS and reset the card styles.
1005 *
1006 * @param {String} styleId
1007 * @returns {void}
1008 */
1009 function resetStyleOnListPage( styleId ) {
1010 const formData = new FormData();
1011 formData.append( 'style_id', styleId );
1012 doJsonPost( 'settings_reset', formData ).then(
1013 response => {
1014 const card = getCardByStyleId( styleId );
1015 card.classList.remove( 'frm-dark-style' );
1016 if ( 'string' === typeof response.style ) {
1017 card.style = response.style;
1018 }
1019 reloadCSSAfterStyleReset();
1020 showStyleResetSuccessMessage();
1021 }
1022 );
1023 }
1024
1025 function showStyleResetSuccessMessage() {
1026 success( __( 'Style has been reset successfully', 'formidable' ) );
1027 }
1028
1029 /**
1030 * Reset the style in-page (without actually updating it).
1031 *
1032 * @returns {void}
1033 */
1034 function resetStyleOnEditPage() {
1035 jQuery.ajax({
1036 type: 'POST',
1037 url: ajaxurl,
1038 data: {
1039 action: 'frm_settings_reset',
1040 nonce: frmGlobal.nonce
1041 },
1042 success: syncEditPageAfterResetAction
1043 });
1044 }
1045
1046 /**
1047 * Handle reset success on edit page.
1048 * This function sets all styling inputs to default values.
1049 *
1050 * @todo Stop triggering change events with jQuery. And remove the other jQuery as well.
1051 *
1052 * @param {Object} response
1053 * @returns {void}
1054 */
1055 function syncEditPageAfterResetAction( response ) {
1056 let defaultValues = response.replace( /^\s+|\s+$/g, '' );
1057 if ( defaultValues.indexOf( '{' ) === 0 ) {
1058 defaultValues = JSON.parse( defaultValues );
1059 }
1060
1061 for ( const key in defaultValues ) {
1062 let targetInput = document.querySelector( 'input[name$="[' + key + ']"], select[name$="[' + key + ']"]' );
1063 if ( ! targetInput ) {
1064 continue;
1065 }
1066
1067 if ( 'radio' === targetInput.getAttribute( 'type' ) ) {
1068 // Reset the repeater icon dropdown.
1069 targetInput = document.querySelector( 'input[name$="[' + key + ']"][value="' + defaultValues[ key ] + '"]' );
1070 if ( targetInput ) {
1071 targetInput.checked = true;
1072 jQuery( targetInput ).trigger( 'change' );
1073 }
1074 continue;
1075 }
1076
1077 targetInput.value = defaultValues[ key ];
1078
1079 if ( targetInput.classList.contains( 'wp-color-picker' ) ) {
1080 // Trigger a change event so the color pickers sync. Otherwise they stay the same color after reset.
1081 jQuery( targetInput ).trigger( 'change' );
1082 }
1083 }
1084
1085 jQuery( '#frm_submit_style, #frm_auto_width' ).prop( 'checked', false );
1086 jQuery( document.getElementById( 'frm_fieldset' ) ).trigger( 'change' );
1087 showStyleResetSuccessMessage();
1088 }
1089
1090 /**
1091 * Reload Formidable CSS after a style is reset so the preview updates immediately without needing to reload the page.
1092 *
1093 * @returns {void}
1094 */
1095 function reloadCSSAfterStyleReset() {
1096 const style = document.getElementById( 'frm-custom-theme-css' );
1097 if ( ! style ) {
1098 return;
1099 }
1100
1101 const newStyle = document.createElement( 'link' );
1102 newStyle.rel = 'stylesheet';
1103 newStyle.type = 'text/css';
1104 newStyle.href = style.href + '&key=' + getAutoId(); // Make the URL unique so the old stylesheet doesn't get picked up by cache.
1105
1106 // Listen for the new style to load before removing the old style to avoid having no styles while the new style is loading.
1107 newStyle.addEventListener(
1108 'load',
1109 () => {
1110 style.parentNode.removeChild( style );
1111 newStyle.id = 'frm-custom-theme-css'; // Assign the old ID to the new style so it can be removed in the next reset action.
1112 }
1113 );
1114
1115 const head = document.getElementsByTagName( 'HEAD' )[0];
1116 head.appendChild( newStyle );
1117 }
1118
1119 /**
1120 * @returns {Number}
1121 */
1122 function getAutoId() {
1123 return ++state.autoId;
1124 }
1125
1126 /**
1127 * @param {Object} data {
1128 * @type {Element} anchor
1129 * @type {String} type
1130 * }
1131 * @returns {Element}
1132 */
1133 function wrapDropdownItem({ anchor, type }) {
1134 return div({
1135 className: 'dropdown-item frm-' + type + '-style',
1136 child: anchor
1137 });
1138 }
1139
1140 /**
1141 * This gets triggered through a hook called in frmAdminBuild.styleInit() from formidable_admin.js.
1142 *
1143 * @returns {void}
1144 */
1145 function initEditPage() {
1146 const { debounce } = frmDom.util;
1147 const debouncedPreviewUpdate = debounce( () => changeStyling(), 100 );
1148 const debouncedColorChange = debounce(( event, value ) => {
1149 /**
1150 * Fires on style colorpicker change.
1151 *
1152 * @param {Event} data.event The color change event.
1153 * @param {string} data.value New color value.
1154 */
1155 wp.hooks.doAction( 'frm_style_options_color_change', { event, value } );
1156 }, 200 );
1157
1158 const debouncedTextSquishCheck = debounce( textSquishCheck, 300 );
1159 initPosClass(); // It's important that this gets called before we add event listeners because it triggers change events.
1160
1161 ['frm_field_height', 'frm_field_font_size', 'frm_field_pad'].forEach( selector => {
1162 document.getElementById( selector ).addEventListener( 'change', debouncedTextSquishCheck );
1163 });
1164
1165 jQuery( 'input.hex' ).wpColorPicker({
1166 change: function( event, ui ) {
1167 let color = jQuery( this ).wpColorPicker( 'color' );
1168 trackUnsavedChange();
1169 if ( ui.color._alpha < 1 ) {
1170 // If there's transparency, use RGBA
1171 color = ui.color.toCSS( 'rgba' );
1172 }
1173 debouncedColorChange( event, color );
1174
1175 if ( null !== event.target.getAttribute( 'data-alpha-color-type' ) ) {
1176 debouncedPreviewUpdate();
1177 return;
1178 }
1179
1180 jQuery( event.target ).val( color ).trigger( 'change' );
1181 }
1182 });
1183 jQuery( '.wp-color-result-text' ).text( function( _, oldText ) {
1184 const container = jQuery( this ).closest( '.wp-picker-container' );
1185 if ( 'undefined' !== typeof container && container[0].parentElement.classList.contains( 'frm-colorpicker' ) ) {
1186 return container[0].querySelector( '.wp-color-picker' ).value;
1187 }
1188 return oldText === 'Select Color' ? 'Select' : oldText;
1189 });
1190 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 );
1191
1192 // 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.
1193 initDatepickerSample();
1194
1195 addHamburgerMenuForEditPage();
1196
1197 document.getElementById( 'frm_styling_form' ).querySelectorAll( 'input, select' ).forEach(
1198 input => input.addEventListener( 'change', () => trackUnsavedChange() )
1199 );
1200
1201 /**
1202 * Sends an AJAX POST request for new CSS to use for the preview.
1203 * This is called whenever a style setting is changed, generally using debouncedPreviewUpdate to avoid simultaneous requests.
1204 *
1205 * @returns {void}
1206 */
1207 function changeStyling() {
1208 const styleInputs = Array.from( document.getElementById( 'frm_style_sidebar' ).querySelectorAll( 'input, select, textarea' ) ).filter(
1209 input => 'style_name' === input.name || 0 === input.name.indexOf( 'frm_style_setting[post_content]' )
1210 );
1211 const locStr = JSON.stringify( jQuery( styleInputs ).serializeArray() );
1212
1213 jQuery.ajax({
1214 type: 'POST',
1215 url: ajaxurl,
1216 data: {
1217 action: 'frm_change_styling',
1218 nonce: frmGlobal.nonce,
1219 frm_style_setting: locStr
1220 },
1221 success: ( css ) => {
1222 handleChangeStylingSuccess( css );
1223 setSelectPlaceholderColor();
1224 }
1225 });
1226 }
1227
1228 /**
1229 * Update the CSS used for the preview on the edit page when a styling input has been updated.
1230 *
1231 * @param {String} css The response from the frm_change_styling request.
1232 * @returns {void}
1233 */
1234 function handleChangeStylingSuccess( css ) {
1235 // Validate the string response. A valid output will include rules with .with_frm_style
1236 if ( -1 === css.indexOf( '.with_frm_style' ) ) {
1237 // Handle error (possibly a permission error, or an outdated nonce).
1238 alert( css );
1239 return;
1240 }
1241 document.getElementById( 'this_css' ).innerHTML = css;
1242 }
1243
1244 /**
1245 * 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".
1246 * This can be triggered when modifying font size, height, and padding.
1247 *
1248 * @returns {void}
1249 */
1250 function textSquishCheck() {
1251 if ( null !== frmDom.util.getCookie( 'frm-style-text-squish-check' ) ) {
1252 return;
1253 }
1254 const height = document.getElementById( 'frm_field_height' ).value.replace( /\D/g, '' );
1255 const paddingEntered = document.getElementById( 'frm_field_pad' ).value.split( ' ' );
1256 const paddingCount = paddingEntered.length;
1257
1258 frmDom.util.setCookie( 'frm-style-text-squish-check', 1, 30 );
1259
1260 // If too many or too few padding entries, leave now
1261 if ( paddingCount === 0 || paddingCount > 4 || height === '' ) {
1262 return;
1263 }
1264
1265 const size = document.getElementById( 'frm_field_font_size' ).value.replace( /\D/g, '' );
1266 // Get the top and bottom padding from entered values
1267 const paddingTop = paddingEntered[0].replace( /\D/g, '' );
1268 let paddingBottom = paddingTop;
1269 if ( paddingCount >= 3 ) {
1270 paddingBottom = paddingEntered[2].replace( /\D/g, '' );
1271 }
1272
1273 // Check if there is enough space for text
1274 const textSpace = height - size - paddingTop - paddingBottom - 3;
1275 if ( textSpace < 0 ) {
1276 frmAdminBuild.infoModal( frm_admin_js.css_invalid_size );
1277 }
1278 }
1279
1280 /**
1281 * When the Collapse icons are updated, sync the dropdown.
1282 * Otherwise the previously selected value will still appear as the selected value.
1283 *
1284 * @returns {void}
1285 */
1286 jQuery( document ).on( 'change', '.frm-dropdown-menu input[type="radio"]', function() {
1287 trackUnsavedChange();
1288
1289 const radio = this;
1290 const btnGrp = radio.closest( '.btn-group' );
1291 const btnId = btnGrp.getAttribute( 'id' );
1292
1293 const select = document.getElementById( btnId.replace( '_select', '' ) );
1294 if ( select ) {
1295 select.value = radio.value;
1296 }
1297
1298 jQuery( btnGrp ).children( 'button' ).html( radio.nextElementSibling.innerHTML + ' <b class="caret"></b>' );
1299
1300 const activeItem = btnGrp.querySelector( '.dropdown-item.active' );
1301 if ( activeItem ) {
1302 activeItem.classList.remove( 'active' );
1303 }
1304
1305 radio.closest( '.dropdown-item' ).classList.add( 'active' );
1306 });
1307
1308 document.querySelectorAll( '.styling_settings h3.accordion-section-title' ).forEach( el => {
1309 el.setAttribute( 'aria-expanded', el.parentElement.classList.contains( 'open' ) );
1310 el.setAttribute( 'role', 'button' );
1311 el.addEventListener( 'click', event => {
1312 maybeCollapseSettings( event );
1313 });
1314 el.addEventListener( 'keydown', event => {
1315 if ( event.key === ' ' ) {
1316 event.preventDefault();
1317 maybeCollapseSettings( event );
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