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

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