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

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

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