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

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