| @@ -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( '§ion=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 | } |