PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
← All changes | js/admin/style.js +238 -25 6.29 → trunk View file →
@@ -60,11 +60,100 @@
60 60 document.body.classList.remove( 'wp-core-ui' );
61 61 document.getElementById( 'frm_style_sidebar' ).classList.add( 'wp-core-ui' );
62 62
63 63 jQuery( document ).on( 'input change', 'input[data-frmrange]', initSliderPreview );
64 +
65 + initOptionLayoutPreview();
64 66 }
65 67
66 68 /**
69 + * Sync the radio/checkbox option layout (alignment) style settings to the preview form in real time.
70 + *
71 + * The layout is applied to the front end through a container class, so changing the style setting
72 + * does not re-render the preview form. This swaps the container class on preview fields that use the
73 + * style setting. The styler preview marks those fields with .frm-default-option-align, so fields with
74 + * their own alignment override are left untouched.
75 + *
76 + * @return {void}
77 + */
78 + function initOptionLayoutPreview() {
79 + bindOptionLayoutSelect( document.getElementById( 'frm_radio_align' ), '.frm_radio' );
80 + bindOptionLayoutSelect( document.getElementById( 'frm_check_align' ), '.frm_checkbox' );
81 + }
82 +
83 + /**
84 + * Listen for changes on an option layout style setting and update the preview to match.
85 + *
86 + * @param {HTMLSelectElement|null} select The style setting dropdown.
87 + * @param {string} optionSelector The single option selector ('.frm_radio' or '.frm_checkbox').
88 + * @return {void}
89 + */
90 + function bindOptionLayoutSelect( select, optionSelector ) {
91 + if ( ! select ) {
92 + return;
93 + }
94 +
95 + select.addEventListener( 'change', () => {
96 + updatePreviewOptionLayout( optionSelector, select.value );
97 + } );
98 + }
99 +
100 + /**
101 + * Replace the alignment container class on preview fields that are using the style setting.
102 + *
103 + * @param {string} optionSelector The single option selector ('.frm_radio' or '.frm_checkbox').
104 + * @param {string} newAlign The newly selected style alignment value.
105 + * @return {void}
106 + */
107 + function updatePreviewOptionLayout( optionSelector, newAlign ) {
108 + const newClass = optionLayoutAlignToClass( newAlign );
109 +
110 + if ( ! newClass ) {
111 + return;
112 + }
113 +
114 + const activeForm = document.getElementById( 'frm_active_style_form' );
115 + if ( ! activeForm ) {
116 + return;
117 + }
118 +
119 + const alignClasses = [ 'vertical_radio', 'horizontal_radio', 'frm_two_col', 'frm_three_col', 'frm_four_col' ];
120 + const containers = new Set();
121 +
122 + activeForm.querySelectorAll( optionSelector ).forEach( option => {
123 + const container = option.closest( '.frm_form_field' );
124 +
125 + // Only fields using the style setting (no override) are marked in the styler preview.
126 + if ( container?.classList.contains( 'frm-default-option-align' ) ) {
127 + containers.add( container );
128 + }
129 + } );
130 +
131 + containers.forEach( container => {
132 + container.classList.remove( ...alignClasses );
133 + container.classList.add( newClass );
134 + } );
135 + }
136 +
137 + /**
138 + * Map an option layout style value to its front-end container class.
139 + *
140 + * @param {string} align The style alignment value.
141 + * @return {string} The matching container class.
142 + */
143 + function optionLayoutAlignToClass( align ) {
144 + if ( 'inline' === align ) {
145 + return 'horizontal_radio';
146 + }
147 +
148 + if ( 'block' === align ) {
149 + return 'vertical_radio';
150 + }
151 +
152 + return align;
153 + }
154 +
155 + /**
67 156 * Initialize the slider functionality in the style preview.
68 157 *
69 158 * @param {HTMLElement} event
70 159 * @return {void}
@@ -94,11 +183,10 @@
94 183 * @return {void}
95 184 */
96 185 function initListPage() {
97 186 document.addEventListener( 'click', handleClickEventsForListPage );
98 - // Add a timeout so Pro has a chance to add a filter first.
99 - // 0 does not always work in Google Chrome, so use 1.
100 - setTimeout( addHamburgerMenusToCards, 1 );
187 + // The dropdown options are added on the first open, so there is no longer a filter from Pro to wait for here.
188 + addHamburgerMenusToCards();
101 189 initDatepickerSample();
102 190
103 191 const enableToggle = document.getElementById( 'frm_enable_styling' );
104 192 const styleIdInput = getStyleIdInput();
@@ -280,9 +368,9 @@
280 368 * @return {void}
281 369 */
282 370 function switchAdvancedSettingsFormAction( target ) {
283 371 const form = document.querySelector( '#frm_styling_form' );
284 - if ( null === form ) {
372 + if ( ! form ) {
285 373 return;
286 374 }
287 375 if ( target.closest( 'a#frm_style_back_to_quick_settings' ) ) {
288 376 form.action = form.action.replace( '&section=advanced-settings', '' );
@@ -633,8 +721,72 @@
633 721 hamburgerMenu.setAttribute( 'role', 'button' );
634 722 hamburgerMenu.setAttribute( 'tabindex', 0 );
635 723
636 724 const isTemplate = data.templateKey !== undefined;
725 +
726 + const dropdownMenu = div( {
727 + // Use dropdown-menu-right to avoid an overlapping issue with the card to the right (where the # of forms would appear above the menu).
728 + className: 'frm-dropdown-menu frm-style-options-menu frm-p-1'
729 + } );
730 +
731 + const isRtl = document.body.classList.contains( 'rtl' );
732 + dropdownMenu.classList.add( `dropdown-menu-${ isRtl ? 'left' : 'right' }` );
733 +
734 + dropdownMenu.setAttribute( 'role', 'menu' );
735 +
736 + fillDropdownMenuOnFirstOpen( hamburgerMenu, dropdownMenu, data, isTemplate );
737 +
738 + return div( {
739 + className: 'dropdown frm_wrap', // The .frm_wrap class prevents a blue outline on the active dropdown trigger.
740 + children: [ hamburgerMenu, dropdownMenu ]
741 + } );
742 + }
743 +
744 + /**
745 + * Add the options to a style card dropdown the first time that dropdown is opened.
746 + *
747 + * The options are built through the frm_style_card_dropdown_options filter, and Pro registers its
748 + * callback while its own script file runs. Adding the options while the page loads raced that file,
749 + * so a request for it that was slow to come back left every card holding the Lite only options.
750 + * Waiting for the first open takes the script order out of it, since Pro has always registered the
751 + * filter by the time a card can be clicked.
752 + *
753 + * @since 6.35
754 + *
755 + * @param {HTMLElement} hamburgerMenu The dropdown trigger.
756 + * @param {HTMLElement} dropdownMenu The dropdown to add the options to.
757 + * @param {DOMStringMap} data The dataset of the style card, or an object with the style ID on the edit page.
758 + * @param {boolean} isTemplate Whether the card is for a style template.
759 + * @return {void}
760 + */
761 + function fillDropdownMenuOnFirstOpen( hamburgerMenu, dropdownMenu, data, isTemplate ) {
762 + const fillDropdownMenu = () => {
763 + if ( dropdownMenu.children.length ) {
764 + return;
765 + }
766 +
767 + const options = getDropdownMenuOptions( data, isTemplate );
768 + dropdownMenu.append( ...options.map( wrapDropdownItem ) );
769 + };
770 +
771 + // Bootstrap fires this on the trigger before it measures and positions the dropdown.
772 + hamburgerMenu.addEventListener( 'show.bs.dropdown', fillDropdownMenu );
773 +
774 + // Fall back to the click in case the dropdown is ever opened without Bootstrap. Listen in the
775 + // capture phase so the options are in place before Bootstrap's own delegated handler runs.
776 + hamburgerMenu.addEventListener( 'click', fillDropdownMenu, true );
777 + }
778 +
779 + /**
780 + * Get the options for a single style card dropdown.
781 + *
782 + * @since 6.35
783 + *
784 + * @param {DOMStringMap} data The dataset of the style card, or an object with the style ID on the edit page.
785 + * @param {boolean} isTemplate Whether the card is for a style template.
786 + * @return {Array} The dropdown options, each an object with an anchor and a type.
787 + */
788 + function getDropdownMenuOptions( data, isTemplate ) {
637 789 let dropdownMenuOptions = [];
638 790
639 791 if ( isListPage ) {
640 792 const applyOption = a( {
@@ -675,23 +827,9 @@
675 827 if ( isListPage && ! isTemplate ) {
676 828 maybeAddDuplicateUpsell( dropdownMenuOptions );
677 829 }
678 830
679 - const dropdownMenu = div( {
680 - // Use dropdown-menu-right to avoid an overlapping issue with the card to the right (where the # of forms would appear above the menu).
681 - className: 'frm-dropdown-menu frm-style-options-menu frm-p-1',
682 - children: dropdownMenuOptions.map( wrapDropdownItem )
683 - } );
684 -
685 - const isRtl = document.body.classList.contains( 'rtl' );
686 - dropdownMenu.classList.add( `dropdown-menu-${ isRtl ? 'left' : 'right' }` );
687 -
688 - dropdownMenu.setAttribute( 'role', 'menu' );
689 -
690 - return div( {
691 - className: 'dropdown frm_wrap', // The .frm_wrap class prevents a blue outline on the active dropdown trigger.
692 - children: [ hamburgerMenu, dropdownMenu ]
693 - } );
831 + return dropdownMenuOptions;
694 832 }
695 833
696 834 /**
697 835 * @param {Array} dropdownMenuOptions
@@ -873,8 +1011,26 @@
873 1011 // The button gets disabled on an input event when the name is empty.
874 1012 return;
875 1013 }
876 1014
1015 + if ( ! styleId || '0' === String( styleId ) ) {
1016 + // A new or duplicated style has no ID yet, so there is nothing to rename on the server.
1017 + // Update the post_title input (which overrides $_GET['style_name'] on save) and the
1018 + // visible style name instead of calling the rename_style endpoint.
1019 + const postTitleInput = document.querySelector( 'input[name="frm_style_setting[post_title]"]' );
1020 + if ( postTitleInput ) {
1021 + postTitleInput.value = newStyleName;
1022 + }
1023 +
1024 + const styleNameElement = document.getElementById( 'frm_style_name' );
1025 + if ( styleNameElement ) {
1026 + styleNameElement.textContent = newStyleName;
1027 + }
1028 +
1029 + success( __( 'Style has been renamed successfully', 'formidable' ) );
1030 + return;
1031 + }
1032 +
877 1033 const formData = new FormData();
878 1034 formData.append( 'style_id', styleId );
879 1035 formData.append( 'style_name', newStyleName );
880 1036 doJsonPost( 'rename_style', formData ).then(
@@ -1166,24 +1322,81 @@
1166 1322 [ 'frm_field_height', 'frm_field_font_size', 'frm_field_pad' ].forEach( selector => {
1167 1323 document.getElementById( selector ).addEventListener( 'change', debouncedTextSquishCheck );
1168 1324 } );
1169 1325
1170 - jQuery( 'input.hex' ).wpColorPicker( {
1326 + function detectColorFormat( value ) {
1327 + if ( value.startsWith( 'rgba' ) ) {
1328 + return 'rgba';
1329 + }
1330 + if ( value.startsWith( 'rgb' ) ) {
1331 + return 'rgb';
1332 + }
1333 + if ( value.startsWith( 'hsla' ) ) {
1334 + return 'hsla';
1335 + }
1336 + if ( value.startsWith( 'hsl' ) ) {
1337 + return 'hsl';
1338 + }
1339 + return 'hex';
1340 + }
1341 +
1342 + const nativeInputValueDescriptor = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value' );
1343 +
1344 + jQuery( 'input.hex' ).each( function() {
1345 + this.dataset.colorFormat = detectColorFormat( this.value );
1346 +
1347 + // Prevent iris from overwriting non-hex formats with hex during color picking.
1348 + const input = this;
1349 + Object.defineProperty( input, 'value', {
1350 + get() {
1351 + return nativeInputValueDescriptor.get.call( this );
1352 + },
1353 + set( val ) {
1354 + const format = input.dataset.colorFormat;
1355 + if ( format && 'hex' !== format && /^#[0-9a-f]{3,8}$/i.test( val ) ) {
1356 + return;
1357 + }
1358 + nativeInputValueDescriptor.set.call( this, val );
1359 + },
1360 + configurable: true
1361 + } );
1362 + } ).on( 'keyup', function() {
1363 + const newFormat = detectColorFormat( this.value );
1364 + if ( this.dataset.colorFormat !== newFormat ) {
1365 + this.dataset.colorFormat = newFormat;
1366 + const container = this.closest( '.wp-picker-container' );
1367 + if ( container ) {
1368 + container.querySelector( '.wp-color-result-text' ).textContent = this.value;
1369 + }
1370 + debouncedColorChange( { target: this }, this.value );
1371 + }
1372 + } ).wpColorPicker( {
1171 1373 change( event, ui ) {
1172 - let color = jQuery( this ).wpColorPicker( 'color' );
1374 + const input = event.target;
1375 + const format = input.dataset.colorFormat || 'hex';
1376 + let color;
1377 +
1173 1378 trackUnsavedChange();
1379 +
1174 1380 if ( ui.color._alpha < 1 ) {
1175 - // If there's transparency, use RGBA
1381 + input.dataset.colorFormat = 'rgba';
1176 1382 color = ui.color.toCSS( 'rgba' );
1383 + } else if ( 'hex' === format ) {
1384 + color = ui.color.toString();
1385 + } else {
1386 + color = ui.color.toCSS( format );
1177 1387 }
1388 +
1178 1389 debouncedColorChange( event, color );
1179 1390
1180 - if ( null !== event.target.getAttribute( 'data-alpha-color-type' ) ) {
1391 + input.value = color;
1392 +
1393 + if ( null !== input.getAttribute( 'data-alpha-color-type' ) ) {
1181 1394 debouncedPreviewUpdate();
1182 1395 return;
1183 1396 }
1184 1397
1185 - jQuery( event.target ).val( color ).trigger( 'change' );
1398 + debouncedPreviewUpdate();
1186 1399 }
1187 1400 } );
1188 1401 jQuery( '.wp-color-result-text' ).text( function( _, oldText ) {
1189 1402 const container = jQuery( this ).closest( '.wp-picker-container' );
@@ -1456,9 +1669,9 @@
1456 1669 }
1457 1670
1458 1671 // Function to change the color of a select element
1459 1672 const changeSelectColor = select => {
1460 - if ( select.options[ select.selectedIndex ] && select.options[ select.selectedIndex ].classList.contains( 'frm-select-placeholder' ) ) {
1673 + if ( select.options[ select.selectedIndex ]?.classList.contains( 'frm-select-placeholder' ) ) {
1461 1674 select.style.setProperty( 'color', textColorDisabled, 'important' );
1462 1675 } else {
1463 1676 select.style.color = '';
1464 1677 }