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

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