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

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

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