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

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