From 70ba4973a3b6cd1af00cd8151120cbc6f9f57081 Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Wed, 17 Sep 2025 18:00:42 +0300 Subject: [PATCH 01/13] feat: improve the lazy loading tab --- .../components/GroupSettingsContainer.js | 25 + .../parts/connected/settings/Lazyload.js | 650 ++++++++++------ assets/src/dashboard/style.scss | 11 + assets/src/global.d.ts | 694 ++++++++++++++++++ inc/admin.php | 62 +- 5 files changed, 1211 insertions(+), 231 deletions(-) create mode 100644 assets/src/dashboard/parts/components/GroupSettingsContainer.js create mode 100644 assets/src/global.d.ts diff --git a/assets/src/dashboard/parts/components/GroupSettingsContainer.js b/assets/src/dashboard/parts/components/GroupSettingsContainer.js new file mode 100644 index 000000000..1e95fda86 --- /dev/null +++ b/assets/src/dashboard/parts/components/GroupSettingsContainer.js @@ -0,0 +1,25 @@ +import classNames from 'classnames'; + +export const GroupSettingsContainer = ({ children, className = '' }) => { + return ( +
+ { children } +
+ ); +}; + +export const GroupSettingsTitle = ({ children, className = '' }) => { + return ( +

+ { children } +

+ ); +}; + +export const GroupSettingsOption = ({ children, className = '' }) => { + return ( +
+ { children } +
+ ); +}; diff --git a/assets/src/dashboard/parts/connected/settings/Lazyload.js b/assets/src/dashboard/parts/connected/settings/Lazyload.js index de480592d..968b4df94 100644 --- a/assets/src/dashboard/parts/connected/settings/Lazyload.js +++ b/assets/src/dashboard/parts/connected/settings/Lazyload.js @@ -14,20 +14,28 @@ import { ColorPicker, ColorIndicator, Button, - RadioControl, - Popover + Popover, + CheckboxControl } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { + useState, + createInterpolateElement, + useMemo, + useCallback +} from '@wordpress/element'; +import RadioBoxes from '../../components/RadioBoxes'; +import { + GroupSettingsContainer, + GroupSettingsTitle, + GroupSettingsOption +} from '../../components/GroupSettingsContainer'; +import Notice from '../../components/Notice'; -const Lazyload = ({ - settings, - setSettings, - setCanSave -}) => { - const { isLoading } = useSelect( select => { +const Lazyload = ({ settings, setSettings, setCanSave }) => { + const { isLoading } = useSelect( ( select ) => { const { isLoading } = select( 'optimole' ); return { @@ -35,236 +43,466 @@ const Lazyload = ({ }; }); - const isLazyloadPlaceholderEnabled = 'disabled' !== settings[ 'lazyload_placeholder' ]; - const isNativeLazyloadEnabled = 'disabled' !== settings[ 'native_lazyload' ]; - const isBGReplacerEnabled = 'disabled' !== settings[ 'bg_replacer' ]; - const isVideoLazyloadEnabled = 'disabled' !== settings[ 'video_lazyload' ]; - const isNoScriptEnabled = 'disabled' !== settings[ 'no_script' ]; - const placeholderColor = settings[ 'placeholder_color' ]; + const isLazyloadPlaceholderEnabled = useMemo( + () => 'disabled' !== settings['lazyload_placeholder'], + [ settings ] + ); + const isNativeLazyloadEnabled = useMemo( + () => 'disabled' !== settings['native_lazyload'], + [ settings ] + ); + const isBGReplacerEnabled = useMemo( + () => 'disabled' !== settings['bg_replacer'], + [ settings ] + ); + const isVideoLazyloadEnabled = useMemo( + () => 'disabled' !== settings['video_lazyload'], + [ settings ] + ); + const isNoScriptEnabled = useMemo( + () => 'disabled' !== settings['no_script'], + [ settings ] + ); + const placeholderColor = useMemo( + () => settings['placeholder_color'], + [ settings.placeholder_color ] + ); + const isScaleEnabled = useMemo( + () => 'disabled' === settings.scale, + [ settings.scale ] + ); + const isLazyloadEnabled = useMemo( + () => 'disabled' !== settings.lazyload, + [ settings.lazyload ] + ); + const isViewPortLoadingEnabled = useMemo( + () => settings['lazyload_type']?.includes( 'viewport' ), + [ settings?.lazyload_type ] + ); + const isFixedSkipLazyEnabled = useMemo( + () => settings['lazyload_type']?.includes( 'fixed' ), + [ settings?.lazyload_type ] + ); const [ phPicker, setPhPicker ] = useState( false ); - const updateOption = ( option, value ) => { - setCanSave( true ); - const data = { ...settings }; - data[ option ] = value ? 'enabled' : 'disabled'; - setSettings( data ); - }; + const toggleOption = useCallback( + ( option, value ) => { + setCanSave( true ); + const data = { ...settings }; + data[option] = value ? 'enabled' : 'disabled'; + setSettings( data ); + }, + [ setCanSave, settings, setSettings ] + ); + + const updateValue = useCallback( + ( option, value ) => { + setCanSave( true ); + setSettings( ( prevSettings ) => ({ + ...prevSettings, + [option]: value + }) ); + }, + [ setCanSave, setSettings ] + ); - const updateValue = ( option, value ) => { - setCanSave( true ); - const data = { ...settings }; - data[ option ] = value; - setSettings( data ); - }; + const setColor = useCallback( + ( value ) => { + updateValue( 'placeholder_color', value ); + }, + [ updateValue ] + ); + + const toggleLoadingBehavior = useCallback( + ( value, slug ) => { + const setting = new Set( + ( settings?.lazyload_type ?? '' ) + ?.split( '|' ) + .filter( ( i ) => 'viewport' === i || 'fixed' === i ) ?? [] + ); + if ( value ) { + setting.add( slug ); + } else { + setting.delete( slug ); + } + console.log( setting ); + updateValue( 'lazyload_type', Array.from( setting ).join( '|' ) ); + }, + [ settings?.lazyload_type ] + ); - const setColor = ( value ) => { - updateValue( 'placeholder_color', value ); - }; + const NotRecommendedWarning = useCallback( ( props ) => { + return ( + <> + {props.label} + + {optimoleDashboardApp.strings.options_strings.not_recommended} + + + ); + }, []); + + const Tag = useCallback( + ({ text, disabled }) => ( + + {text} + + ), + [] + ); + + const DescriptionWithTags = useCallback( + ({ text, tags }) => { + return ( + <> + {text} +
+ {tags.map( ({ text, disabled }) => ( + + ) )} +
+ + ); + }, + [ Tag ] + ); + + if ( ! isLazyloadEnabled ) { + return ( + <> + toggleOption( 'lazyload', value )} + /> + + ); + } return ( <> - -

- -

- toggleOption( 'lazyload', value )} + /> +
+ +
+ - {optimoleDashboardApp.strings.options_strings.lazyload_behaviour_fixed.replace( '[N]', settings['skip_lazyload_images'])} - {'fixed' === settings['lazyload_type'] && ( - <> -

- {optimoleDashboardApp.strings.options_strings.lazyload_behaviour_fixed_desc} -

-
- updateValue( 'skip_lazyload_images', value )} - /> -
- - )} -
+ title: + optimoleDashboardApp.strings.options_strings + .smart_loading_title, + description: ( + ), - value: 'fixed' + value: 'disabled' }, { - label: ( -
- {optimoleDashboardApp.strings.options_strings.lazyload_behaviour_viewport} - {'viewport' === settings['lazyload_type'] && ( -

- {optimoleDashboardApp.strings.options_strings.lazyload_behaviour_viewport_desc} -

- )} -
+ title: ( + ), - value: 'viewport' - }, - { - label: ( -
- {optimoleDashboardApp.strings.options_strings.lazyload_behaviour_all} - {'all' === settings['lazyload_type'] && ( -

- {optimoleDashboardApp.strings.options_strings.lazyload_behaviour_all_desc} -

- )} -
+ description: ( + ), - value: 'all' + value: 'enabled' } - ] } - onChange={value => updateValue( 'lazyload_type', value )} - /> -
-
- -
- -

} - checked={ isLazyloadPlaceholderEnabled } - disabled={ isLoading } - className={ classnames( - { - 'is-disabled': isLoading + ]} + value={isNativeLazyloadEnabled ? 'enabled' : 'disabled'} + onChange={( value ) => + toggleOption( 'native_lazyload', 'enabled' === value ) } - ) } - onChange={ value => updateOption( 'lazyload_placeholder', value ) } - /> - - { isLazyloadPlaceholderEnabled && -

- + /> - { phPicker && - setPhPicker( false )} - > - + + { + optimoleDashboardApp.strings.options_strings + .lazyload_behaviour_title + }{' '} + ({optimoleDashboardApp.strings.options_strings.global_option}) + + + { + toggleLoadingBehavior( value, 'fixed' ); + }} + disabled={false} + __nextHasNoMarginBottom={true} + /> +
+ + updateValue( 'skip_lazyload_images', value ) + } + __nextHasNoMarginBottom={true} /> - - - } -
- } - +
+ + + { + toggleLoadingBehavior( value, 'viewport' ); + }} + disabled={false} + __nextHasNoMarginBottom={true} + /> + + {isFixedSkipLazyEnabled && isViewPortLoadingEnabled && ( + + )} + -
+ + + {optimoleDashboardApp.strings.options_strings.visual_settings} + + +
+ + toggleOption( 'lazyload_placeholder', value ) + } + disabled={isLoading} + __nextHasNoMarginBottom={true} + /> +
-

} - checked={ isNativeLazyloadEnabled } - disabled={ isLoading } - className={ classnames( - { - 'is-disabled': isLoading - } - ) } - onChange={ value => updateOption( 'native_lazyload', value ) } - /> + {isLazyloadPlaceholderEnabled && ( +

+ -
+ {phPicker && ( + setPhPicker( false )} + > + + + + )} +
+ )} +
+ + <noscript> + } + )} + onChange={( value ) => toggleOption( 'no_script', value )} + disabled={isLoading} + __nextHasNoMarginBottom={true} + /> + +
+ +
+

} - checked={ isVideoLazyloadEnabled } - disabled={ isLoading } - className={ classnames( - { - 'is-disabled': isLoading - } - ) } - onChange={ value => updateOption( 'video_lazyload', value ) } + label={optimoleDashboardApp.strings.options_strings.toggle_scale} + help={optimoleDashboardApp.strings.options_strings.scale_desc} + checked={isScaleEnabled} + disabled={isLoading} + className={classnames({ + 'is-disabled': isLoading + })} + onChange={( value ) => toggleOption( 'scale', ! value )} /> -


- -

} - checked={ isNoScriptEnabled } - disabled={ isLoading } - className={ classnames( - { - 'is-disabled': isLoading - } - ) } - onChange={ value => updateOption( 'no_script', value ) } - /> - -


-

} - checked={ isBGReplacerEnabled } - disabled={ isLoading } - className={ classnames( - { - 'is-disabled': isLoading + + + {optimoleDashboardApp.strings.options_strings.extended_features} + + updateOption( 'bg_replacer', value ) } - /> - { isBGReplacerEnabled && ( - <> -


- - + help={() => (

+ )} + checked={isBGReplacerEnabled} + disabled={isLoading} + className={classnames( 'text-sm', { + 'is-disabled': isLoading + })} + onChange={( value ) => toggleOption( 'bg_replacer', value )} + /> + {isBGReplacerEnabled && ( + <> updateValue( 'watchers', value ) } + placeholder={ + optimoleDashboardApp.strings.options_strings + .watch_placeholder_lazyload + + '\n' + + optimoleDashboardApp.strings.options_strings + .watch_placeholder_lazyload_example + } + value={settings.watchers} + onChange={( value ) => updateValue( 'watchers', value )} + help={ + optimoleDashboardApp.strings.options_strings.watch_desc_lazyload + } /> - - - ) } + + )} + + ( +

+ )} + checked={isVideoLazyloadEnabled} + disabled={isLoading} + className={classnames( 'mt-8', { + 'is-disabled': isLoading + })} + onChange={( value ) => toggleOption( 'video_lazyload', value )} + __nextHasNoMarginBottom={true} + /> + ); }; diff --git a/assets/src/dashboard/style.scss b/assets/src/dashboard/style.scss index 5367f897c..69ac06e46 100644 --- a/assets/src/dashboard/style.scss +++ b/assets/src/dashboard/style.scss @@ -459,3 +459,14 @@ $mango-yellow: #FBBF24; @apply text-xs rounded-sm bg-gray-100/50; } } + + +textarea#optml-css-watchers { + border-color: rgb(229 231 235); + border-radius: 0.25rem; + color: gray; + + &::placeholder { + color: #6B7280; + } +} diff --git a/assets/src/global.d.ts b/assets/src/global.d.ts new file mode 100644 index 000000000..fb029b7a6 --- /dev/null +++ b/assets/src/global.d.ts @@ -0,0 +1,694 @@ + +export {}; + +declare global { + var optimoleDashboardApp: IOptimoleDashboardApp; +} + +// @see https://transform.tools/json-to-typescript +export interface IOptimoleDashboardApp { + strings: Strings + assets_url: string + dam_url: string + connection_status: string + has_application: string + user_status: string + available_apps: AvailableApp[] + api_key: string + routes: Routes + language: string + nonce: string + user_data: UserData + remove_latest_images: string + current_user: CurrentUser + site_settings: SiteSettings + offload_limit: string + home_url: string + optimoleHome: string + optimoleDashHome: string + optimoleDashBilling: string + days_since_install: string + is_offload_media_available: string + auto_connect: string + cron_disabled: string + submenu_links: SubmenuLink[] + bf_notices: any[] + spc_banner: SpcBanner + show_exceed_plan_quota_notice: string +} + +export interface Strings { + optimole: string + version: string + terms_menu: string + privacy_menu: string + testdrive_menu: string + service_details: string + dashboard_title: string + banner_title: string + banner_description: string + quick_action_title: string + connect_btn: string + disconnect_btn: string + select: string + your_domain: string + add_api: string + your_api_key: string + looking_for_api_key: string + refresh_stats_cta: string + updating_stats_cta: string + api_key_placeholder: string + account_needed_heading: string + account_needed_sub_heading: string + account_needed_trust_badge: string + account_needed_setup_time: string + invalid_key: string + keep_connected: string + cloud_library: string + image_storage: string + disconnect_title: string + disconnect_desc: string + email_address_label: string + steps_connect_api_title: string + register_btn: string + secure_connection: string + step_one_api_title: string + optml_dashboard: string + steps_connect_api_desc: string + api_exists: string + back_to_register: string + back_to_connect: string + error_register: string + invalid_email: string + connected: string + connecting: string + not_connected: string + usage: string + quota: string + logged_in_as: string + private_cdn_url: string + existing_user: string + notification_message_register: string + premium_support: string + account_needed_title: string + account_needed_subtitle_1: string + account_needed_subtitle_3: string + account_needed_subtitle_2: string + account_needed_subtitle_4: string + account_needed_footer: string + account_connecting_title: string + account_connecting_subtitle: string + notice_just_activated: string + notice_api_not_working: string + notice_disabled_account: string + signup_terms: string + dashboard_menu_item: string + settings_menu_item: string + help_menu_item: string + settings_exclusions_menu_item: string + settings_resize_menu_item: string + settings_compression_menu_item: string + advanced_settings_menu_item: string + general_settings_menu_item: string + lazyload_settings_menu_item: string + watermarks_menu_item: string + conflicts_menu_item: string + conflicts: Conflicts + upgrade: Upgrade + neve: Neve + metrics: Metrics + quick_actions: QuickActions + options_strings: OptionsStrings + help: Help + watermarks: Watermarks + latest_images: LatestImages + csat: Csat + cron_error: string + cancel: string + optimization_status: OptimizationStatus + optimization_tips: string +} + +export interface Conflicts { + title: string + message: string + conflict_close: string + no_conflicts_found: string +} + +export interface Upgrade { + title: string + title_long: string + reason_1: string + reason_2: string + reason_3: string + reason_4: string + cta: string +} + +export interface Neve { + is_active: string + byline: string + reason_1: string + reason_2: string + reason_3: string + reason_4: string + reason_5: string +} + +export interface Metrics { + metricsTitle1: string + metricsSubtitle1: string + metricsTitle2: string + metricsSubtitle2: string + metricsTitle3: string + metricsSubtitle3: string + metricsTitle4: string + metricsSubtitle4: string +} + +export interface QuickActions { + speed_test_title: string + speed_test_desc: string + speed_test_link: string + clear_cache_images: string + clear_cache: string + offload_images: string + offload_images_desc: string + advance_settings: string + configure_settings: string +} + +export interface OptionsStrings { + compression_mode: string + compression_mode_speed_optimized: string + compression_mode_quality_optimized: string + compression_mode_custom: string + compression_mode_speed_optimized_desc: string + compression_mode_quality_optimized_desc: string + compression_mode_custom_desc: string + best_format_title: string + best_format_desc: string + add_filter: string + add_site: string + admin_bar_desc: string + auto_q_title: string + cache_desc: string + cache_title: string + clear_cache_notice: string + image_size_notice: string + clear_cache_images: string + clear_cache_assets: string + connect_step_0: string + connect_step_1: string + connect_step_2: string + connect_step_3: string + disabled: string + enable_avif_title: string + enable_avif_desc: string + enable_bg_lazyload_desc: string + enable_bg_lazyload_title: string + enable_video_lazyload_desc: string + enable_video_lazyload_title: string + enable_noscript_desc: string + enable_noscript_title: string + enable_gif_replace_title: string + enable_offload_media_title: string + enable_offload_media_desc: string + enable_cloud_images_title: string + enable_cloud_images_desc: string + enable_image_replace: string + enable_lazyload_placeholder_desc: string + lazyload_behaviour_title: string + lazyload_behaviour_desc: string + lazyload_behaviour_all: string + lazyload_behaviour_all_desc: string + lazyload_behaviour_viewport: string + lazyload_behaviour_viewport_desc: string + lazyload_behaviour_fixed: string + lazyload_behaviour_fixed_desc: string + enable_lazyload_placeholder_title: string + enable_network_opt_desc: string + enable_network_opt_title: string + enable_resize_smart_desc: string + enable_resize_smart_title: string + enable_retina_desc: string + enable_retina_title: string + enable_limit_dimensions_desc: string + enable_limit_dimensions_title: string + enable_limit_dimensions_notice: string + enable_badge_title: string + enable_badge_settings: string + enable_badge_show_icon: string + enable_badge_position: string + badge_position_text_1: string + badge_position_text_2: string + enable_badge_description: string + image_sizes_title: string + enabled: string + exclude_class_desc: string + exclude_ext_desc: string + exclude_filename_desc: string + exclude_desc_optimize: string + exclude_title_lazyload: string + exclude_desc_lazyload: string + exclude_title_optimize: string + exclude_url_desc: string + name: string + cropped: string + exclude_url_match_desc: string + exclude_first: string + images: string + exclude_first_images_title: string + exclude_first_images_desc: string + filter_class: string + filter_ext: string + filter_filename: string + filter_operator_contains: string + filter_operator_matches: string + filter_operator_is: string + filter_url: string + filter_helper: string + gif_replacer_desc: string + height_field: string + add_image_size_button: string + add_image_size_desc: string + here: string + hide: string + high_q_title: string + image_1_label: string + image_2_label: string + lazyload_desc: string + filter_length_error: string + scale_desc: string + low_q_title: string + medium_q_title: string + no_images_found: string + native_desc: string + option_saved: string + ml_quality_desc: string + quality_desc: string + quality_selected_value: string + quality_slider_desc: string + quality_title: string + strip_meta_title: string + strip_meta_desc: string + replacer_desc: string + sample_image_loading: string + save_changes: string + show: string + selected_sites_title: string + selected_sites_desc: string + selected_all_sites_desc: string + select_all_sites_desc: string + select_site: string + cloud_site_title: string + cloud_site_desc: string + toggle_ab_item: string + toggle_lazyload: string + toggle_scale: string + toggle_native: string + on_toggle: string + off_toggle: string + view_sample_image: string + watch_placeholder_lazyload: string + watch_placeholder_lazyload_example: string + watch_desc_lazyload: string + smart_loading_title: string + smart_loading_desc: string + browser_native_lazy: string + viewport_detection: string + placeholders_color: string + auto_scaling: string + lightweight_native: string + watch_title_lazyload: string + width_field: string + crop: string + toggle_cdn: string + cdn_desc: string + enable_css_minify_title: string + css_minify_desc: string + enable_js_minify_title: string + js_minify_desc: string + sync_title: string + rollback_title: string + sync_desc: string + rollback_desc: string + sync_media: string + rollback_media: string + sync_media_progress: string + estimated_time: string + calculating_estimated_time: string + images_processing: string + active_optimize_exclusions: string + active_lazyload_exclusions: string + minutes: string + stop: string + show_logs: string + hide_logs: string + view_logs: string + rollback_media_progress: string + rollback_media_error: string + rollback_media_error_desc: string + remove_notice: string + sync_media_error: string + sync_media_link: string + rollback_media_link: string + sync_media_error_desc: string + offload_disable_warning_title: string + offload_disable_warning_desc: string + offload_enable_info_desc: string + offload_conflicts_part_1: string + offload_conflicts_part_2: string + offloading_success: string + rollback_success: string + offloading_radio_legend: string + offload_radio_option_rollback_title: string + offload_radio_option_rollback_desc: string + offload_radio_option_offload_title: string + offload_radio_option_offload_desc: string + offload_limit_reached: string + select: string + yes: string + no: string + lazyload_placeholder_color: string + clear: string + settings_saved: string + settings_saved_error: string + cache_cleared: string + cache_cleared_error: string + offloading_start_title: string + offloading_start_description: string + offloading_start_action: string + offloading_stop_title: string + offloading_stop_description: string + offloading_stop_action: string + rollback_start_title: string + rollback_start_description: string + rollback_start_action: string + rollback_stop_title: string + rollback_stop_description: string + rollback_stop_action: string + cloud_library_btn_text: string + cloud_library_btn_link: string + exceed_plan_quota_notice_title: string + exceed_plan_quota_notice_description: string + exceed_plan_quota_notice_start_action: string + exceed_plan_quota_notice_secondary_action: string + visual_settings: string + extended_features: string + global_option: string + enable_lazy_loading_title: string + not_recommended: string + vieport_skip_images_notice: string +} + +export interface Help { + section_one_title: string + section_two_title: string + section_two_sub: string + get_support_title: string + get_support_desc: string + get_support_cta: string + feat_request_title: string + feat_request_desc: string + feat_request_cta: string + feedback_title: string + feedback_desc: string + feedback_cta: string + account_title: string + account_item_one: string + account_item_two: string + account_item_three: string + image_processing_title: string + image_processing_item_one: string + image_processing_item_two: string + image_processing_item_three: string + api_title: string + api_item_one: string + api_item_two: string + api_item_three: string +} + +export interface Watermarks { + image: string + loading_remove_watermark: string + max_allowed: string + list_header: string + settings_header: string + no_images_found: string + id: string + name: string + type: string + action: string + upload: string + add_desc: string + wm_title: string + wm_desc: string + opacity_field: string + opacity_title: string + opacity_desc: string + position_title: string + position_desc: string + pos_nowe_title: string + pos_no_title: string + pos_noea_title: string + pos_we_title: string + pos_ce_title: string + pos_ea_title: string + pos_sowe_title: string + pos_so_title: string + pos_soea_title: string + offset_x_field: string + offset_y_field: string + offset_title: string + offset_desc: string + scale_field: string + scale_title: string + scale_desc: string + save_changes: string +} + +export interface LatestImages { + image: string + no_images_found: string + compression: string + loading_latest_images: string + last: string + saved: string + smaller: string + optimized_images: string + same_size: string + small_optimization: string + medium_optimization: string + big_optimization: string +} + +export interface Csat { + title: string + close: string + heading_one: string + heading_two: string + heading_three: string + low: string + high: string + feedback_placeholder: string + skip: string + submit: string + thank_you: string +} + +export interface OptimizationStatus { + title: string + statusTitle1: string + statusSubTitle1: string + statusTitle2: string + statusSubTitle2: string + statusTitle3: string + statusSubTitle3: string +} + +export interface AvailableApp { + key: string + status: string + domain: string + is_cname_assigned: string + cf_ssl_registered: string + certificate_arn: string + limit_wl_sites: number +} + +export interface Routes { + update_option: string + request_update: string + check_redirects: string + connect: string + select_application: string + register_service: string + disconnect: string + poll_optimized_images: string + get_sample_rate: string + upload_onboard_images: string + number_of_images_and_pages: string + clear_offload_errors: string + get_offload_conflicts: string + move_image: string + poll_conflicts: string + dismiss_conflict: string + clear_cache_request: string + insert_images: string + dismiss_notice: string + optimizations: string +} + +export interface UserData { + id: number + display_name: string + user_email: string + picture: string + validated: string + cdn_key: string + cdn_secret: string + whitelist: string[] + plan: string + status: string + visitors: number + visitors_limit: number + visitors_left: number + visitors_pretty: string + visitors_limit_pretty: string + visitors_left_pretty: string + app_count: number + available_apps: AvailableApp2[] + traffic: number + images_number: number + offload_limit: number + offloaded_images: number + compression_percentage: number + saved_size: number + domain: string + is_cname_assigned: string + extra_visits: boolean + renews_on: number +} + +export interface AvailableApp2 { + key: string + status: string + domain: string + is_cname_assigned: string + cf_ssl_registered: string + certificate_arn: string + limit_wl_sites: number +} + +export interface CurrentUser { + email: string +} + +export interface SiteSettings { + quality: string + admin_bar_item: string + lazyload: string + network_optimization: string + retina_images: string + limit_dimensions: string + limit_height: number + limit_width: number + lazyload_placeholder: string + skip_lazyload_images: number + bg_replacer: string + video_lazyload: string + resize_smart: string + no_script: string + lazyload_type: string + compression_mode: string + image_replacer: string + cdn: string + filters: Filters + cloud_sites: CloudSites + defined_image_sizes: any[] + watchers: string + watermark: Watermark + img_to_video: string + scale: string + css_minify: string + js_minify: string + native_lazyload: string + avif: string + autoquality: string + offload_media: string + cloud_images: string + strip_metadata: string + whitelist_domains: string[] + banner_frontend: string + offloading_status: string + rollback_status: string + best_format: string + offload_limit_reached: string + placeholder_color: string + show_offload_finish_notice: string + show_badge_icon: string + badge_position: string +} + +export interface Filters { + lazyload: Lazyload + optimize: Optimize +} + +export interface Lazyload { + extension: any[] + filename: any[] + page_url: any[] + page_url_match: any[] + class: any[] +} + +export interface Optimize { + extension: any[] + filename: any[] + page_url: any[] + page_url_match: any[] + class: any[] +} + +export interface CloudSites { + all: string + "localhost:10038": string +} + +export interface Watermark { + id: number + opacity: number + position: string + x_offset: number + y_offset: number + scale: number +} + +export interface SubmenuLink { + href: string + text: string + hash: string +} + +export interface SpcBanner { + activate_url: string + status: string + banner_dismiss_key: string + i18n: I18n +} + +export interface I18n { + dismiss: string + title: string + byline: string + features: string[] + cta: string + activate: string + installing: string + activating: string + activated: string + error: string +} diff --git a/inc/admin.php b/inc/admin.php index 06451c60c..39a2e28ce 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1791,27 +1791,18 @@ private function get_dashboard_strings() { '', '' ), - 'enable_bg_lazyload_desc' => sprintf( - /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ - __( 'Enable this to lazy-load images set as CSS backgrounds. If Optimole misses any, you can directly target specific CSS selectors to ensure all background images are optimized. %1$sLearn more%2$s', 'optimole-wp' ), - '', - '' - ), + 'enable_bg_lazyload_desc' => __( 'Apply lazy loading to CSS background images. The toggle below enables it globally. The selector list is optional and only needed to extend coverage if certain elements are missed.', 'optimole-wp' ), 'enable_bg_lazyload_title' => __( 'CSS Background Lazy Load', 'optimole-wp' ), - 'enable_video_lazyload_desc' => sprintf( - /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ - __( 'By default, lazy loading does not work for embedded videos and iframes. Enable this option to activate the lazy-load on these elements. %1$sLearn more%2$s', 'optimole-wp' ), - '', - '' - ), - 'enable_video_lazyload_title' => __( 'Lazy Loading for Embedded Videos and Iframes', 'optimole-wp' ), + 'enable_video_lazyload_desc' => __( 'Lazy load embedded videos and iframe content', 'optimole-wp' ), + 'enable_video_lazyload_title' => __( 'Video & iframes', 'optimole-wp' ), 'enable_noscript_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enables fallback images for browsers that can\'t handle JavaScript-based lazy loading or related features. Disabling it may resolve conflicts with other plugins or configurations and decrease HTML page size. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), - 'enable_noscript_title' => __( 'Noscript Tag', 'optimole-wp' ), + // translators: %s is the name of the html tag (e.g:


-

} - checked={ isLazyloadEnabled } - disabled={ ! isReplacerEnabled || isLoading } - className={ classnames( - { - 'is-disabled': ! isReplacerEnabled || isLoading - } - ) } - onChange={ value => updateOption( 'lazyload', value ) } - /> - -


- { isUserActive && <> { const { isLoading } = useSelect( ( select ) => { @@ -43,11 +50,11 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { }; }); - const isLazyloadPlaceholderEnabled = useMemo( + const isLazyLoadPlaceholderEnabled = useMemo( () => 'disabled' !== settings['lazyload_placeholder'], [ settings ] ); - const isNativeLazyloadEnabled = useMemo( + const isNativeLazyLoadEnabled = useMemo( () => 'disabled' !== settings['native_lazyload'], [ settings ] ); @@ -55,7 +62,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { () => 'disabled' !== settings['bg_replacer'], [ settings ] ); - const isVideoLazyloadEnabled = useMemo( + const isVideoLazyLoadEnabled = useMemo( () => 'disabled' !== settings['video_lazyload'], [ settings ] ); @@ -71,7 +78,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { () => 'disabled' === settings.scale, [ settings.scale ] ); - const isLazyloadEnabled = useMemo( + const isLazyLoadEnabled = useMemo( () => 'disabled' !== settings.lazyload, [ settings.lazyload ] ); @@ -85,6 +92,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { ); const [ phPicker, setPhPicker ] = useState( false ); + const [ showModal, setShowModal ] = useState( false ); const toggleOption = useCallback( ( option, value ) => { @@ -126,7 +134,6 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { } else { setting.delete( slug ); } - console.log( setting ); updateValue( 'lazyload_type', Array.from( setting ).toSorted().join( '|' ) ); }, [ settings?.lazyload_type ] @@ -176,13 +183,13 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { [ Tag ] ); - if ( ! isLazyloadEnabled ) { + if ( ! isLazyLoadEnabled ) { return ( <> { toggleOption( 'lazyload', value )} + onChange={() => setShowModal( DISABLE_OPTION_MODAL_TYPE.lazyLoad )} /> + { + showModal && ( + setShowModal( false ) } + onConfirm={ () => { + window?.formbricks?.track( 'disable_lazy_load_feature', { + hiddenFields: { + feature: `${ showModal }` + } + }); + + if ( DISABLE_OPTION_MODAL_TYPE.javascriptLoading === showModal ) { + toggleOption( 'native_lazyload', true ); + } else if ( DISABLE_OPTION_MODAL_TYPE.scale === showModal ) { + toggleOption( 'scale', false ); + } else if ( DISABLE_OPTION_MODAL_TYPE.lazyLoad === showModal ) { + toggleOption( 'lazyload', false ); + } + + setShowModal( false ); + } } + onSecondaryAction={ () => setShowModal( false ) } + /> + ) + }
{ value: 'enabled' } ]} - value={isNativeLazyloadEnabled ? 'enabled' : 'disabled'} - onChange={( value ) => - toggleOption( 'native_lazyload', 'enabled' === value ) - } + value={isNativeLazyLoadEnabled ? 'enabled' : 'disabled'} + onChange={( value ) => { + if ( 'disabled' === value ) { + toggleOption( 'native_lazyload', false ); + } else { + setShowModal( DISABLE_OPTION_MODAL_TYPE.javascriptLoading ); + } + }} /> @@ -334,7 +378,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { title={''} text={ optimoleDashboardApp.strings.options_strings - .vieport_skip_images_notice + .viewport_skip_images_notice } /> )} @@ -351,7 +395,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { optimoleDashboardApp.strings.options_strings .enable_lazyload_placeholder_title } - checked={isLazyloadPlaceholderEnabled} + checked={isLazyLoadPlaceholderEnabled} onChange={( value ) => toggleOption( 'lazyload_placeholder', value ) } @@ -360,7 +404,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { /> - {isLazyloadPlaceholderEnabled && ( + {isLazyLoadPlaceholderEnabled && (
+ { + isNativeLazyLoadEnabled && ( + + ) + } +
{ className={classnames({ 'is-disabled': isLoading })} - onChange={( value ) => toggleOption( 'scale', ! value )} + onChange={() => setShowModal( DISABLE_OPTION_MODAL_TYPE.scale ) } /> @@ -494,7 +551,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { }} /> )} - checked={isVideoLazyloadEnabled} + checked={isVideoLazyLoadEnabled} disabled={isLoading} className={classnames( 'mt-8', { 'is-disabled': isLoading diff --git a/assets/src/dashboard/parts/connected/settings/Menu.js b/assets/src/dashboard/parts/connected/settings/Menu.js index 90ebf8b3c..8be80542f 100644 --- a/assets/src/dashboard/parts/connected/settings/Menu.js +++ b/assets/src/dashboard/parts/connected/settings/Menu.js @@ -59,16 +59,12 @@ const menuItems = [ const SubMenu = ({ children, tab, - settings, setTab }) => { return (
    {children.map( item => { const { value, label } = item; - if ( 'lazyload' === item.value && 'disabled' === settings.lazyload ) { - return; - } const classes = classnames( { 'bg-light-blue hover:text-purple-gray': tab === value, diff --git a/assets/src/global.d.ts b/assets/src/global.d.ts index fb029b7a6..9a6270693 100644 --- a/assets/src/global.d.ts +++ b/assets/src/global.d.ts @@ -127,6 +127,7 @@ export interface Strings { cancel: string optimization_status: OptimizationStatus optimization_tips: string + native_lazy_load_warning: string } export interface Conflicts { @@ -398,9 +399,8 @@ export interface OptionsStrings { visual_settings: string extended_features: string global_option: string - enable_lazy_loading_title: string not_recommended: string - vieport_skip_images_notice: string + viewport_skip_images_notice: string } export interface Help { diff --git a/inc/admin.php b/inc/admin.php index 39a2e28ce..971950f95 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1973,7 +1973,7 @@ private function get_dashboard_strings() { 'cloud_site_title' => __( 'Show images only from these sites:', 'optimole-wp' ), 'cloud_site_desc' => __( 'Browse images only from the specified websites. Otherwise, images from all websites will appear in the library.', 'optimole-wp' ), 'toggle_ab_item' => __( 'Admin bar status', 'optimole-wp' ), - 'toggle_lazyload' => __( 'Enable Lazy Loading', 'optimole-wp' ), + 'toggle_lazyload' => __( 'Enable Lazy Loading & Scaling', 'optimole-wp' ), 'toggle_scale' => __( 'Smart Image Scaling', 'optimole-wp' ), 'toggle_native' => __( 'Browser Native Lazy Load', 'optimole-wp' ), 'on_toggle' => __( 'On', 'optimole-wp' ), @@ -2073,10 +2073,17 @@ private function get_dashboard_strings() { 'extended_features' => __( 'Extended Features', 'optimole-wp' ), // translators: mark that the options are aplied globally. 'global_option' => __( 'Global', 'optimole-wp' ), - 'enable_lazy_loading_title' => __( 'Enable Lazy Loading', 'optimole-wp' ), // translators: This option is discouraged from being used. 'not_recommended' => __( 'Not recommended', 'optimole-wp' ), - 'vieport_skip_images_notice' => __( "When viewport-based loading and skip first images are both enabled: the skip rule applies on a user's first page view. On subsequent views, once viewport data exists, the skip rule is ignored in favor of the viewport data.", 'optimole-wp' ), + // translators: %1$s is the starting bold tag, %2$s is the ending bold tag. + 'viewport_skip_images_notice' => sprintf( __( 'When %1$sviewport-based loading%2$s and %1$sskip first images%2$s are both enabled: the skip rule applies on a user\'s first page view. On subsequent views, once viewport data exists, the skip rule is ignored in favor of the viewport data.', 'optimole-wp' ), '', '' ), + // translators: %1$s is the starting bold tag, %2$s is the ending bold tag. + 'native_lazy_load_warning' => sprintf( __( 'Native browser loading works with viewport detection, but it does not support smart image scaling. It can offer better cross-browser compatibility. Still, we do %1$snot recommend%2$s it for most sites due to fewer controls and features compared to Smart Loading.', 'optimole-wp' ), '', '' ), + 'performance_impact_alert_title' => __( 'Performance Impact Alert', 'optimole-wp' ), + 'performance_impact_alert_lazy_desc' => __( 'Disabling Lazy Load may significantly impact your site\'s loading speed and Core Web Vitals scores.', 'optimole-wp' ), + 'performance_impact_alert_scale_desc' => __( 'Disabling Image Scaling may significantly impact your site\'s loading speed and Core Web Vitals scores.', 'optimole-wp' ), + 'performance_impact_alert_action_label' => __( 'Continue with disabling', 'optimole-wp' ), + 'performance_impact_alert_secondary_action_label' => __( 'Keep enabled', 'optimole-wp' ), ], 'help' => [ 'section_one_title' => __( 'Help and Support', 'optimole-wp' ), From 06aa9544a11256ca63ec8958b337425f4d622b2a Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Tue, 23 Sep 2025 13:14:18 +0300 Subject: [PATCH 04/13] chore: conditions checks --- inc/lazyload_replacer.php | 20 +- inc/tag_replacer.php | 17 +- inc/v2/PageProfiler/Profile.php | 18 + tests/test-lazyload-viewport.php | 615 +++++++++++++++++++++++-------- 4 files changed, 509 insertions(+), 161 deletions(-) diff --git a/inc/lazyload_replacer.php b/inc/lazyload_replacer.php index 5566790dd..99c9c2a61 100644 --- a/inc/lazyload_replacer.php +++ b/inc/lazyload_replacer.php @@ -450,25 +450,33 @@ public function can_lazyload_for( $url, $tag = '' ) { return $type['ext'] !== 'png'; } + $no_viewport_data_available = true; + if ( $this->settings->is_lazyload_type_viewport() ) { - $is_in_all_viewports = Optml_Manager::instance()->page_profiler->is_in_all_viewports( $this->get_id_by_url( $url ) ); - $is_lcp_image = Optml_Manager::instance()->page_profiler->is_lcp_image_in_all_viewports( $this->get_id_by_url( $url ) ); + $image_id = $this->get_id_by_url( $url ); + $is_in_all_viewports = Optml_Manager::instance()->page_profiler->is_in_all_viewports( $image_id ); + $is_lcp_image = Optml_Manager::instance()->page_profiler->is_lcp_image_in_all_viewports( $image_id ); + $no_viewport_data_available = ! Optml_Manager::instance()->page_profiler->check_data_availability(); if ( OPTML_DEBUG ) { if ( $is_in_all_viewports ) { - do_action( 'optml_log', 'Lazyload skipped image is in all viewports ' . $url . '|' . $this->get_id_by_url( $url ) ); + do_action( 'optml_log', 'Lazyload skipped image is in all viewports ' . $url . '|' . $image_id ); } elseif ( $is_lcp_image ) { - do_action( 'optml_log', 'Lazyload skipped image is LCP ' . $url . '|' . $this->get_id_by_url( $url ) ); + do_action( 'optml_log', 'Lazyload skipped image is LCP ' . $url . '|' . $image_id ); } } if ( $is_in_all_viewports || $is_lcp_image ) { - Links::add_id( $this->get_id_by_url( $url ), 'high' ); // collect ID for preload. + Links::add_id( $image_id, 'high' ); // collect ID for preload. return false; } } - if ( $this->settings->is_lazyload_type_fixed() && Optml_Tag_Replacer::$lazyload_skipped_images < self::get_skip_lazyload_limit() ) { + if ( + $no_viewport_data_available && + $this->settings->is_lazyload_type_fixed() && + Optml_Tag_Replacer::$lazyload_skipped_images < self::get_skip_lazyload_limit() + ) { return false; } return true; diff --git a/inc/tag_replacer.php b/inc/tag_replacer.php index fe3a55ec9..5d6a6c5cd 100644 --- a/inc/tag_replacer.php +++ b/inc/tag_replacer.php @@ -401,26 +401,31 @@ public function regular_tag_replace( $new_tag, $original_url, $new_url, $optml_a } } + $no_viewport_data_available = true; + if ( $this->settings->is_lazyload_type_viewport() ) { - $is_in_all_viewports = Optml_Manager::instance()->page_profiler->is_in_all_viewports( $this->get_id_by_url( $original_url ) ); - $is_lcp_image = Optml_Manager::instance()->page_profiler->is_lcp_image_in_all_viewports( $this->get_id_by_url( $original_url ) ); + $image_id = $this->get_id_by_url( $original_url ); + $is_in_all_viewports = Optml_Manager::instance()->page_profiler->is_in_all_viewports( $image_id ); + $is_lcp_image = Optml_Manager::instance()->page_profiler->is_lcp_image_in_all_viewports( $image_id ); + $no_viewport_data_available = ! Optml_Manager::instance()->page_profiler->check_data_availability(); if ( OPTML_DEBUG ) { if ( $is_in_all_viewports ) { - do_action( 'optml_log', 'Adding preload priority for image ' . $original_url . '|' . $this->get_id_by_url( $original_url ) ); + do_action( 'optml_log', 'Adding preload priority for image ' . $original_url . '|' . $image_id ); } elseif ( $is_lcp_image ) { - do_action( 'optml_log', 'Adding preload image is LCP ' . $original_url . '|' . $this->get_id_by_url( $original_url ) ); + do_action( 'optml_log', 'Adding preload image is LCP ' . $original_url . '|' . $image_id ); } } if ( $is_in_all_viewports || $is_lcp_image ) { $new_tag = preg_replace( '/get_id_by_url( $original_url ), 'high' ); // collect ID for preload. + Links::add_id( $image_id, 'high' ); // collect ID for preload. } } - // // If the image is between the first images we add the fetchpriority attribute to improve the LCP. + // If the image is between the first images we add the fetchpriority attribute to improve the LCP. if ( + $no_viewport_data_available && $this->settings->is_lazyload_type_fixed() && self::$lazyload_skipped_images < Optml_Lazyload_Replacer::get_skip_lazyload_limit() && false === strpos( $new_tag, 'fetchpriority=' ) diff --git a/inc/v2/PageProfiler/Profile.php b/inc/v2/PageProfiler/Profile.php index e5e720eeb..134c8a85a 100644 --- a/inc/v2/PageProfiler/Profile.php +++ b/inc/v2/PageProfiler/Profile.php @@ -379,4 +379,22 @@ public static function get_active_devices(): array { self::DEVICE_TYPE_DESKTOP, ]; } + + /** + * Check if there is any profile data available for the current profile. + * + * @return bool + */ + public function check_data_availability(): bool { + $has_data = false; + + foreach ( self::get_active_devices() as $device ) { + if ( empty( self::$current_profile_data[ $device ] ) ) { + $has_data = true; + break; + } + } + + return $has_data; + } } diff --git a/tests/test-lazyload-viewport.php b/tests/test-lazyload-viewport.php index 992b7600d..5ca5732f2 100644 --- a/tests/test-lazyload-viewport.php +++ b/tests/test-lazyload-viewport.php @@ -1,8 +1,12 @@ update('service_data', [ @@ -25,168 +30,480 @@ public function setUp() : void 'whitelist' => ['example.com'], ]); - $settings->update('lazyload', 'enabled'); + $settings->update('lazyload', 'enabled'); $settings->update('lazyload_type', 'viewport'); Optml_Url_Replacer::instance()->init(); Optml_Tag_Replacer::instance()->init(); Optml_Lazyload_Replacer::instance()->init(); Optml_Manager::instance()->init(); add_filter('optml_page_profile_id', [self::class, 'mock_page_ids']); - + + // Reset static counter for skipped images to ensure test isolation + $this->resetLazyloadSkippedImages(); } - public function tearDown() : void - { + + /** + * Clean up the test environment after each test. + */ + public function tearDown(): void { parent::tearDown(); - remove_filter('optml_page_profile_id', [self::class, 'mock_page_ids']); + remove_all_filters('optml_page_profile_id'); + // Reset static counter to clean state + $this->resetLazyloadSkippedImages(); + Profile::reset_current_profile(); } - public static function mock_page_ids(){ - return 1; - } - public static function get_sample_html(){ - return file_get_contents(OPTML_PATH . '/tests/assets/sample.html'); - } - public function test_lazyload_viewport(){ - + + /** + * Test lazy loading behavior with viewport data. + * + * This test verifies that: + * 1. All images are lazy-loaded when no viewport data is available. + * 2. Above-the-fold images are not lazy-loaded when viewport data exists. + * 3. Above-the-fold images receive `fetchpriority="high"`. + */ + public function test_lazyload_viewport() { + // When no viewport data exists, all images should be lazy-loaded + // This is the default fallback behavior for viewport-based lazy loading $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); - //check if the html has the same number of image tags as the same numer of data-opt-id $this->assertEquals(substr_count($html, 'page_profiler->store(self::mock_page_ids(), Profile::DEVICE_TYPE_DESKTOP, [1579989818]); - $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); + + $this->storeMockProfileData(self::mock_page_ids(), Profile::DEVICE_TYPE_DESKTOP, [1579989818]); + $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); $this->assertEquals(substr_count($html, 'data-opt-src'), substr_count($html, 'data-opt-id')); - - Optml_Manager::instance()->page_profiler->store(self::mock_page_ids(), Profile::DEVICE_TYPE_MOBILE, [1579989818]); - $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); + $this->storeMockProfileData(self::mock_page_ids(), Profile::DEVICE_TYPE_MOBILE, [1579989818]); + $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); + + // With complete viewport data, above-fold images should not be lazy-loaded $this->assertNotEquals(substr_count($html, 'data-opt-src'), substr_count($html, 'data-opt-id')); - $image_tags = Optml_Manager::instance()->parse_images_from_html($html); - $image_tags = array_filter($image_tags[0], function($tag){ + $imageTags = Optml_Manager::instance()->parse_images_from_html($html); + $filteredTags = array_filter($imageTags[0], function ($tag) { return strpos($tag, 'data-opt-id=1579989818') !== false; - })[0]; - $this->assertStringNotContainsString('data-opt-src', $image_tags); - $this->assertStringContainsString('fetchpriority="high"', $image_tags); + }); + $this->assertNotEmpty($filteredTags); + $specificImageTag = $filteredTags[0]; + $this->assertStringNotContainsString('data-opt-src', $specificImageTag); + $this->assertStringContainsString('fetchpriority="high"', $specificImageTag); } - public function test_lazyload_viewport_with_lcp_data(){ + + /** + * Test lazy loading behavior with LCP (Largest Contentful Paint) data. + * + * This test ensures that an image marked as LCP is not lazy-loaded and + * receives the `fetchpriority="high"` attribute to improve loading performance. + */ + public function test_lazyload_viewport_with_lcp_data() { // Test with LCP (Largest Contentful Paint) data for desktop - Optml_Manager::instance()->page_profiler->store( - self::mock_page_ids(), - Profile::DEVICE_TYPE_DESKTOP, - [1579989818], - [], - ['type' => 'img', 'imageId' => 1579989818] + // LCP images are prioritized for loading and get fetchpriority="high" to improve performance metrics. + $this->storeMockProfileData( + self::mock_page_ids(), + Profile::DEVICE_TYPE_DESKTOP, + [1579989818], + [], + ['type' => 'img', 'imageId' => 1579989818], ); - + + // Test with LCP (Largest Contentful Paint) data for mobile $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); - + // Check if the LCP image has fetchpriority="high" attribute - $image_tags = Optml_Manager::instance()->parse_images_from_html($html); - $lcp_image = array_filter($image_tags[0], function($tag){ + $imageTags = Optml_Manager::instance()->parse_images_from_html($html); + $filteredTags = array_filter($imageTags[0], function ($tag) { return strpos($tag, 'data-opt-id=1579989818') !== false; - })[0]; - - $this->assertStringContainsString('fetchpriority="high"', $lcp_image); - $this->assertStringNotContainsString('data-opt-src', $lcp_image); - } - -public function test_lazyload_viewport_with_different_device_profiles(){ - // Store different images for desktop and mobile - Optml_Manager::instance()->page_profiler->store( - self::mock_page_ids(), - Profile::DEVICE_TYPE_DESKTOP, - [1579989818, 1698280985] - ); - - Optml_Manager::instance()->page_profiler->store( - self::mock_page_ids(), - Profile::DEVICE_TYPE_MOBILE, - [1579989818] - ); - - $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); - - // Image 1579989818 should be eagerly loaded on both devices - $image_tags = Optml_Manager::instance()->parse_images_from_html($html); - $image_1 = array_filter($image_tags[0], function($tag){ - return strpos($tag, 'data-opt-id=1579989818') !== false; - })[0]; - $image_2 = array_values(array_filter($image_tags[0], function($tag){ - return strpos($tag, 'data-opt-id=1698280985') !== false; - }))[0]; - $this->assertStringNotContainsString('data-opt-src', $image_1); - $this->assertStringContainsString('data-opt-src', $image_2); - -} - -public function test_is_in_viewport_detection(){ - // Test the is_in_viewport detection methods - Optml_Manager::instance()->page_profiler->store( - self::mock_page_ids(), - Profile::DEVICE_TYPE_DESKTOP, - [1579989818] - ); - - Optml_Manager::instance()->page_profiler->store( - self::mock_page_ids(), - Profile::DEVICE_TYPE_MOBILE, - [1579989818] - ); - - // Set the current profile ID to enable viewport detection - Profile::set_current_profile_id(self::mock_page_ids()); - Optml_Manager::instance()->page_profiler->set_current_profile_data(); - - // Test if image is detected in all viewports - $this->assertTrue( - Optml_Manager::instance()->page_profiler->is_in_all_viewports(1579989818) - ); - - // Test if image is detected in any viewport - $this->assertNotFalse( - Optml_Manager::instance()->page_profiler->is_in_any_viewport(1579989818) - ); - - // Test with an image that doesn't exist in the viewport - $this->assertFalse( - Optml_Manager::instance()->page_profiler->is_in_all_viewports(9999999) - ); -} -public function test_profile_data_storage_and_retrieval(){ - // Test storing and retrieving profile data - $above_fold_images = [1579989818, 1579989819]; - $bg_selectors = [ - '.hero-banner' => [ - '.above-fold' => ['https://example.com/image1.jpg'] - ] - ]; - $lcp_data = [ - 'type' => 'img', - 'imageId' => 1579989818 - ]; - - // Store the data - Optml_Manager::instance()->page_profiler->store( - self::mock_page_ids(), - Profile::DEVICE_TYPE_DESKTOP, - $above_fold_images, - $bg_selectors, - $lcp_data - ); - - // Retrieve the data - $profile_data = Optml_Manager::instance()->page_profiler->get_profile_data(self::mock_page_ids()); - - // Check if data was stored correctly - $this->assertArrayHasKey(Profile::DEVICE_TYPE_DESKTOP, $profile_data); - $this->assertArrayHasKey('af', $profile_data[Profile::DEVICE_TYPE_DESKTOP]); - $this->assertArrayHasKey('bg', $profile_data[Profile::DEVICE_TYPE_DESKTOP]); - $this->assertArrayHasKey('lcp', $profile_data[Profile::DEVICE_TYPE_DESKTOP]); - - // Check if above-fold images are stored correctly (as keys with value true) - $this->assertTrue($profile_data[Profile::DEVICE_TYPE_DESKTOP]['af'][1579989818]); - $this->assertTrue($profile_data[Profile::DEVICE_TYPE_DESKTOP]['af'][1579989819]); - - // Check if LCP data is stored correctly - $this->assertEquals('img', $profile_data[Profile::DEVICE_TYPE_DESKTOP]['lcp']['type']); - $this->assertEquals(1579989818, $profile_data[Profile::DEVICE_TYPE_DESKTOP]['lcp']['imageId']); -} + }); + $this->assertNotEmpty($filteredTags); + $lcpImageTag = $filteredTags[0]; + + $this->assertStringContainsString('fetchpriority="high"', $lcpImageTag); + $this->assertStringNotContainsString('data-opt-src', $lcpImageTag); + } + + /** + * Test lazy loading with different profiles for different devices. + * + * This test verifies that the correct images are lazy-loaded based on + * device-specific (desktop vs. mobile) viewport profiles. + */ + public function test_lazyload_viewport_with_different_device_profiles() { + // Store different images for desktop and mobile + $this->storeMockProfileData( + self::mock_page_ids(), + Profile::DEVICE_TYPE_DESKTOP, + [1579989818, 1698280985], + ); + $this->storeMockProfileData(self::mock_page_ids(), Profile::DEVICE_TYPE_MOBILE, [1579989818]); + + $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); + + // Image 1579989818 should be eagerly loaded on both devices + $imageTags = Optml_Manager::instance()->parse_images_from_html($html); + + // Check if we have image tags before accessing array + $this->assertNotEmpty($imageTags, 'No image tags found in HTML'); + $this->assertArrayHasKey(0, $imageTags, 'Image tags array structure is unexpected'); + + $filteredTags1 = array_filter($imageTags[0], function ($tag) { + return strpos($tag, 'data-opt-id=1579989818') !== false; + }); + $this->assertNotEmpty($filteredTags1); + $image1 = reset($filteredTags1); // Use reset() instead of direct array access + + $filteredTags2 = array_filter($imageTags[0], function ($tag) { + return strpos($tag, 'data-opt-id=1698280985') !== false; + }); + $this->assertNotEmpty($filteredTags2); + $image2 = reset($filteredTags2); // Use reset() instead of direct array access + + $this->assertStringNotContainsString('data-opt-src', $image1); + $this->assertStringContainsString('data-opt-src', $image2); + } + + /** + * Data provider for viewport detection tests. + * + * @return array Test data with image IDs and expected results. + */ + public function viewportDetectionDataProvider() { + return [ + 'existing_image' => [1579989818, true, true], + 'non_existing_image' => [9999999, false, false], + ]; + } + + /** + * Test the `is_in_all_viewports` and `is_in_any_viewport` methods. + * + * @dataProvider viewportDetectionDataProvider + * @param int $imageId The ID of the image to check. + * @param bool $expectedInAll Expected result from `is_in_all_viewports`. + * @param bool $expectedInAny Expected result from `is_in_any_viewport`. + */ + public function test_is_in_viewport_detection($imageId, $expectedInAll, $expectedInAny) { + // Test the is_in_viewport detection methods + $this->storeMockProfileData(self::mock_page_ids(), Profile::DEVICE_TYPE_DESKTOP, [1579989818]); + $this->storeMockProfileData(self::mock_page_ids(), Profile::DEVICE_TYPE_MOBILE, [1579989818]); + + // Set the current profile ID to enable viewport detection + Profile::set_current_profile_id(self::mock_page_ids()); + Optml_Manager::instance()->page_profiler->set_current_profile_data(); + + // Test if image is detected in all viewports + $actualInAll = Optml_Manager::instance()->page_profiler->is_in_all_viewports($imageId); + if ($expectedInAll) { + $this->assertTrue($actualInAll, "Expected image $imageId to be in all viewports"); + } else { + $this->assertFalse($actualInAll, "Expected image $imageId NOT to be in all viewports"); + } + + // Test if image is detected in any viewport + $actualInAny = Optml_Manager::instance()->page_profiler->is_in_any_viewport($imageId); + if ($expectedInAny) { + $this->assertNotFalse($actualInAny, "Expected image $imageId to be in any viewport"); + } else { + $this->assertFalse($actualInAny, "Expected image $imageId NOT to be in any viewport"); + } + } + + /** + * Test the storage and retrieval of profile data. + * + * This test ensures that above-fold images, background selectors, and LCP data + * are correctly stored and can be retrieved from the page profiler. + */ + public function test_profile_data_storage_and_retrieval() { + // Test storing and retrieving profile data + $aboveFoldImages = [1579989818, 1579989819]; + $bgSelectors = [ + '.hero-banner' => [ + '.above-fold' => ['https://example.com/image1.jpg'], + ], + ]; + $lcpData = [ + 'type' => 'img', + 'imageId' => 1579989818, + ]; + + $this->storeMockProfileData( + self::mock_page_ids(), + Profile::DEVICE_TYPE_DESKTOP, + $aboveFoldImages, + $bgSelectors, + $lcpData, + ); + + $profileData = Optml_Manager::instance()->page_profiler->get_profile_data(self::mock_page_ids()); + + // Check if data was stored correctly + $this->assertArrayHasKey(Profile::DEVICE_TYPE_DESKTOP, $profileData); + $this->assertArrayHasKey('af', $profileData[Profile::DEVICE_TYPE_DESKTOP]); + $this->assertArrayHasKey('bg', $profileData[Profile::DEVICE_TYPE_DESKTOP]); + $this->assertArrayHasKey('lcp', $profileData[Profile::DEVICE_TYPE_DESKTOP]); + + // Check if above-fold images are stored correctly (as keys with value true) + $this->assertTrue($profileData[Profile::DEVICE_TYPE_DESKTOP]['af'][1579989818]); + $this->assertTrue($profileData[Profile::DEVICE_TYPE_DESKTOP]['af'][1579989819]); + + // Check if LCP data is stored correctly + $this->assertEquals('img', $profileData[Profile::DEVICE_TYPE_DESKTOP]['lcp']['type']); + $this->assertEquals(1579989818, $profileData[Profile::DEVICE_TYPE_DESKTOP]['lcp']['imageId']); + } + + /** + * Test fallback to fixed lazyload behavior when no viewport data is available for a profile. + * + * This test verifies that if `lazyload_type` is 'fixed', it correctly skips the + * specified number of images, even when a profile ID is present but has no data. + */ + public function test_no_viewport_data_available_with_fixed_lazyload() { + // Test when lazyload type is fixed and no viewport data is available + $settings = new Optml_Settings(); + $settings->update('lazyload_type', 'fixed'); + $settings->update('skip_lazyload_images', 2); // Skip first 2 images + + $this->withProfileId(888, function () { + $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); + + // With fixed lazyload and skip_lazyload_images = 2, first 2 images should not be lazyloaded + $image_tags = Optml_Manager::instance()->parse_images_from_html($html); + $total_images = count($image_tags[0]); + $lazyloaded_images = substr_count($html, 'data-opt-src'); + + // Should skip the first 2 images from lazyload + $this->assertEquals($total_images - 2, $lazyloaded_images); + }); + + // Reset settings + $settings->update('lazyload_type', 'viewport'); + } + + /** + * Data provider for profile existence tests. + * + * @return array Test data with profile IDs and device combinations. + */ + public function profileExistenceDataProvider() { + return [ + 'no_data' => [777, [], false], + 'desktop_only' => [777, [Profile::DEVICE_TYPE_DESKTOP], false], + 'mobile_only' => [777, [Profile::DEVICE_TYPE_MOBILE], false], + 'both_devices' => [777, [Profile::DEVICE_TYPE_DESKTOP, Profile::DEVICE_TYPE_MOBILE], true], + ]; + } + + /** + * Test the `exists_all` method for checking complete viewport data. + * + * This test verifies that `exists_all` correctly reports whether a profile + * has data for all required device types (desktop and mobile). + * + * @dataProvider profileExistenceDataProvider + * @param int $profileId The profile ID to check. + * @param array $devicesToStore The device types to store data for. + * @param bool $expectedExists The expected result from `exists_all`. + */ + public function test_viewport_data_exists_check($profileId, $devicesToStore, $expectedExists) { + // Store data for specified devices + foreach ($devicesToStore as $deviceType) { + $this->storeMockProfileData($profileId, $deviceType, [1579989818]); + } + + $this->assertEquals( + $expectedExists, + Optml_Manager::instance()->page_profiler->exists_all($profileId), + ); + } + + /** + * Test the behavior when only partial viewport data is available. + * + * This test ensures that if a profile has data for only one device (e.g., desktop), + * the system falls back to lazy-loading all images, treating it as if no + * complete viewport data is available. + */ + public function test_partial_viewport_data_behavior() { + // Test behavior when only partial viewport data is available + $profile_id = 667; + $settings = new Optml_Settings(); + $settings->update('lazyload_type', 'viewport'); + + // Store data for desktop only - this creates partial data + $this->storeMockProfileData($profile_id, Profile::DEVICE_TYPE_DESKTOP, []); + + $this->withProfileId($profile_id, function () use ($profile_id) { + $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); + + // With partial data, should behave as if no viewport data is available + // All images should be lazyloaded + $totalImages = substr_count($html, 'assertEquals( + $totalImages, + $lazyloadedImages, + "Expected all $totalImages images to be lazyloaded, but only $lazyloadedImages were lazyloaded", + ); + }); + } + + /** + * Test the `can_lazyload_for` method when no viewport data is available. + * + * This test verifies that the `can_lazyload_for` method returns `true` (allowing lazy-load) + * when viewport lazy loading is enabled but no data exists for the current profile. + */ + public function test_can_lazyload_for_method_with_no_viewport_data() { + // Test the can_lazyload_for method directly when no viewport data is available + $replacer = Optml_Lazyload_Replacer::instance(); + + $this->withProfileId(555, function () use ($replacer) { + $test_url = 'https://example.com/test-image.jpg'; + $test_tag = 'test'; + + // When no viewport data is available, should return true (allow lazyload) + $this->assertTrue($replacer->can_lazyload_for($test_url, $test_tag)); + }); + } + + /** + * Test the fallback behavior when viewport lazy loading is enabled but no data is available. + * + * This test confirms that when `lazyload_type` is 'viewport' and no profile data exists, + * all images are lazy-loaded, ignoring the `skip_lazyload_images` setting. + */ + public function test_fallback_to_fixed_behavior_when_no_viewport_data() { + // Test that when viewport lazyload is enabled but no data exists, + // it falls back to allowing lazyload (unlike fixed which skips first N images) + $settings = new Optml_Settings(); + $settings->update('lazyload_type', 'viewport'); + $settings->update('skip_lazyload_images', 3); + + $this->withProfileId(444, function () { + $html = Optml_Manager::instance()->replace_content(self::get_sample_html()); + + // With viewport lazyload and no data, ALL images should be lazyloaded + // (different from fixed lazyload which would skip first 3) + $this->assertEquals(substr_count($html, 'assertFalse(Optml_Manager::instance()->page_profiler->exists_all($invalidProfileId)); + + // Attempting to store with invalid ID should not throw errors but handle gracefully + $this->storeMockProfileData($invalidProfileId, Profile::DEVICE_TYPE_DESKTOP, [1579989818]); + + // Get profile data to check behavior with invalid ID + $profileData = Optml_Manager::instance()->page_profiler->get_profile_data($invalidProfileId); + + // The behavior may vary - either empty array or the data might be stored with string key + // Check that either it's empty OR if data exists, it's structured properly + if (empty($profileData)) { + $this->assertEmpty($profileData, 'Expected empty profile data for invalid ID'); + } else { + // If data is stored despite invalid ID, verify it doesn't break the system + $this->assertIsArray($profileData, 'Profile data should be an array even with invalid ID'); + } + } + + /** + * Helper method to reset the static lazyload_skipped_images counter. + */ + private function resetLazyloadSkippedImages() { + $reflection = new ReflectionClass('Optml_Tag_Replacer'); + $property = $reflection->getProperty('lazyload_skipped_images'); + $property->setAccessible(true); + $property->setValue(null, 0); + } + + /** + * Helper method to temporarily set a custom profile ID filter. + * + * @param int $profileId The profile ID to return. + * @param callable $callback The test callback to execute. + */ + private function withProfileId($profileId, $callback) { + // Remove all existing filters first + remove_all_filters('optml_page_profile_id'); + + // Add the new filter with higher priority + add_filter( + 'optml_page_profile_id', + function () use ($profileId) { + return $profileId; + }, + 999, + ); + + // Clear current profile data and set new profile ID + Profile::set_current_profile_id($profileId); + Optml_Manager::instance()->page_profiler->set_current_profile_data(); + + try { + $callback(); + } finally { + // Always restore the original filter, even if callback throws + remove_all_filters('optml_page_profile_id'); + add_filter('optml_page_profile_id', [self::class, 'mock_page_ids']); + + // Reset to default profile + Profile::set_current_profile_id(self::mock_page_ids()); + Optml_Manager::instance()->page_profiler->set_current_profile_data(); + } + } + + /** + * Mock the page profile ID for testing purposes. + * + * @return int The mocked profile ID. + */ + public static function mock_page_ids() { + return 1; + } + + /** + * Get sample HTML content for testing. + * + * @return string The sample HTML. + */ + public static function get_sample_html() { + return file_get_contents(OPTML_PATH . '/tests/assets/sample.html'); + } + + /** + * Helper method to store mock profile data, reducing duplication. + * + * @param int $profileId The profile ID. + * @param string $deviceType The device type (e.g., Profile::DEVICE_TYPE_DESKTOP). + * @param array $above_fold_images Array of image IDs. + * @param array $bgSelectors Background selectors (optional). + * @param array $lcpData LCP data (optional). + */ + private function storeMockProfileData( + $profileId, + $deviceType, + $above_fold_images = [], + $bgSelectors = [], + $lcpData = [], + ) { + Optml_Manager::instance()->page_profiler->store( + $profileId, + $deviceType, + $above_fold_images, + $bgSelectors, + $lcpData, + ); + + // Set the current profile to use the stored data + Profile::reset_current_profile(); + Profile::set_current_profile_id($profileId); + Optml_Manager::instance()->page_profiler->set_current_profile_data(); + } } From 780b3eed5cb08854d4dc64806ace59195afcff44 Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Tue, 23 Sep 2025 15:19:51 +0300 Subject: [PATCH 05/13] fix: image scale toggle --- .../src/dashboard/parts/connected/settings/Lazyload.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/assets/src/dashboard/parts/connected/settings/Lazyload.js b/assets/src/dashboard/parts/connected/settings/Lazyload.js index eac18cf90..944e2e0cc 100644 --- a/assets/src/dashboard/parts/connected/settings/Lazyload.js +++ b/assets/src/dashboard/parts/connected/settings/Lazyload.js @@ -234,7 +234,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { if ( DISABLE_OPTION_MODAL_TYPE.javascriptLoading === showModal ) { toggleOption( 'native_lazyload', true ); } else if ( DISABLE_OPTION_MODAL_TYPE.scale === showModal ) { - toggleOption( 'scale', false ); + toggleOption( 'scale', true ); } else if ( DISABLE_OPTION_MODAL_TYPE.lazyLoad === showModal ) { toggleOption( 'lazyload', false ); } @@ -488,7 +488,13 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { className={classnames({ 'is-disabled': isLoading })} - onChange={() => setShowModal( DISABLE_OPTION_MODAL_TYPE.scale ) } + onChange={( value ) => { + if ( value ) { + toggleOption( 'scale', ! value ); + } else { + setShowModal( DISABLE_OPTION_MODAL_TYPE.scale ); + } + } } /> From 668511d347013c77989e9d8194fa30247e95924e Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Tue, 23 Sep 2025 17:06:27 +0300 Subject: [PATCH 06/13] chore: add pre-filled support contact --- .../dashboard/parts/components/RadioBoxes.js | 2 + .../parts/connected/settings/Lazyload.js | 123 +++++++++++------- assets/src/global.d.ts | 7 + inc/admin.php | 18 ++- 4 files changed, 105 insertions(+), 45 deletions(-) diff --git a/assets/src/dashboard/parts/components/RadioBoxes.js b/assets/src/dashboard/parts/components/RadioBoxes.js index 5e78d301f..3aa2c62db 100644 --- a/assets/src/dashboard/parts/components/RadioBoxes.js +++ b/assets/src/dashboard/parts/components/RadioBoxes.js @@ -43,6 +43,8 @@ export default function RadioBoxes({ options, value, onChange, label, disabled = type="radio" name="label" value={buttonValue} + checked={isActive} + onChange={() => {}} // Add empty onChange to satisfy React's controlled input requirements id={buttonValue} className="!opacity-0 !w-0 !h-0 !overflow-hidden !absolute !pointer-events-none" disabled={disabled} diff --git a/assets/src/dashboard/parts/connected/settings/Lazyload.js b/assets/src/dashboard/parts/connected/settings/Lazyload.js index 944e2e0cc..9618c8cab 100644 --- a/assets/src/dashboard/parts/connected/settings/Lazyload.js +++ b/assets/src/dashboard/parts/connected/settings/Lazyload.js @@ -17,6 +17,7 @@ import { Popover, CheckboxControl } from '@wordpress/components'; +import { sprintf } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; @@ -94,6 +95,30 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { const [ phPicker, setPhPicker ] = useState( false ); const [ showModal, setShowModal ] = useState( false ); + const supportPrefilledContactFormUrl = useMemo( () => { + const contactUrl = new URL( window.optimoleDashboardApp.report_issue_url ); + const { + contact_support + } = window.optimoleDashboardApp.strings; + + let subject = contact_support.disable_lazy_load_scaling; + if ( DISABLE_OPTION_MODAL_TYPE.scale === showModal ) { + subject = contact_support.disable_image_scaling; + } else if ( DISABLE_OPTION_MODAL_TYPE.javascriptLoading === showModal ) { + subject = contact_support.enable_native_lazy_load; + } + + contactUrl.searchParams.set( + 'contact_subject', + sprintf( + contact_support.title_prefix, + subject + ) + ); + + return contactUrl; + }, [ showModal ]); + const toggleOption = useCallback( ( option, value ) => { setCanSave( true ); @@ -212,39 +237,51 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { })} onChange={() => setShowModal( DISABLE_OPTION_MODAL_TYPE.lazyLoad )} /> - { - showModal && ( - setShowModal( false ) } - onConfirm={ () => { - window?.formbricks?.track( 'disable_lazy_load_feature', { - hiddenFields: { - feature: `${ showModal }` - } - }); - - if ( DISABLE_OPTION_MODAL_TYPE.javascriptLoading === showModal ) { - toggleOption( 'native_lazyload', true ); - } else if ( DISABLE_OPTION_MODAL_TYPE.scale === showModal ) { - toggleOption( 'scale', true ); - } else if ( DISABLE_OPTION_MODAL_TYPE.lazyLoad === showModal ) { - toggleOption( 'lazyload', false ); + {showModal && ( + setShowModal( false )} + onConfirm={() => { + window?.formbricks?.track( 'disable_lazy_load_feature', { + hiddenFields: { + feature: `${showModal}` } + }); - setShowModal( false ); - } } - onSecondaryAction={ () => setShowModal( false ) } - /> - ) - } + if ( DISABLE_OPTION_MODAL_TYPE.javascriptLoading === showModal ) { + toggleOption( 'native_lazyload', true ); + } else if ( DISABLE_OPTION_MODAL_TYPE.scale === showModal ) { + toggleOption( 'scale', true ); + } else if ( DISABLE_OPTION_MODAL_TYPE.lazyLoad === showModal ) { + toggleOption( 'lazyload', false ); + } + + setShowModal( false ); + }} + onSecondaryAction={() => { + window.open( supportPrefilledContactFormUrl, '_blank' ); + setShowModal( false ); + }} + /> + )}
    { - { - isNativeLazyLoadEnabled && ( - - ) - } + {isNativeLazyLoadEnabled && ( + + )}
    @@ -494,7 +529,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { } else { setShowModal( DISABLE_OPTION_MODAL_TYPE.scale ); } - } } + }} /> diff --git a/assets/src/global.d.ts b/assets/src/global.d.ts index 9a6270693..5bdf096e1 100644 --- a/assets/src/global.d.ts +++ b/assets/src/global.d.ts @@ -35,6 +35,7 @@ export interface IOptimoleDashboardApp { bf_notices: any[] spc_banner: SpcBanner show_exceed_plan_quota_notice: string + report_issue_url: string } export interface Strings { @@ -128,6 +129,12 @@ export interface Strings { optimization_status: OptimizationStatus optimization_tips: string native_lazy_load_warning: string + contact_support: { + title_prefix: string + disable_lazy_load_scaling: string + disable_image_scaling: string + enable_native_lazy_load: string + } } export interface Conflicts { diff --git a/inc/admin.php b/inc/admin.php index 971950f95..872c74d70 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1382,6 +1382,15 @@ private function localize_dashboard_app() { 'bf_notices' => $this->get_bf_notices(), 'spc_banner' => $this->get_spc_banner(), 'show_exceed_plan_quota_notice' => $this->should_show_exceed_quota_warning(), + 'report_issue_url' => add_query_arg( + [ + 'utm_source' => 'plugin', + 'utm_medium' => 'wp_dashboard', + 'utm_campaign' => 'report_issue', + 'contact_website' => home_url(), + ], + tsdk_translate_link( 'https://optimole.com/contact/' ) + ), ]; } @@ -2183,6 +2192,13 @@ private function get_dashboard_strings() { ' ', '' ), + 'contact_support' => [ + // translators: %s is the email main subject. + 'title_prefix' => __( '[Lazy Load Issue] %s', 'optimole-wp' ), + 'disable_lazy_load_scaling' => __( 'Disable Lazy Load & Scaling' ), + 'disable_image_scaling' => __( 'Disable Image Scaling' ), + 'enable_native_lazy_load' => __( 'Enable Native Lazy Load' ), + ], // translators: %s is the date of the renewal. 'renew_date' => __( 'Renews %s', 'optimole-wp' ), ]; @@ -2225,7 +2241,7 @@ public function get_survey_metadata( $data, $page_slug ) { } $data = [ - 'environmentId' => 'clo8wxwzj44orpm0gjchurujm', + 'environmentId' => 'clo8wxwy044olpm0gn83ihta6', 'attributes' => [ 'plan' => $user_data['plan'], 'status' => $user_data['status'], From a997281bedbafbaa84b5923b701fe967b14a6016 Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Tue, 23 Sep 2025 17:07:55 +0300 Subject: [PATCH 07/13] chore: migrate "lazyload_type" `all` to `fixed` on dashboard settings --- inc/admin.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/inc/admin.php b/inc/admin.php index 872c74d70..0cfc96741 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1345,6 +1345,12 @@ private function localize_dashboard_app() { ]; $lang_code = isset( $available_languages[ $language ] ) ? 'de' : 'en'; + // Migrate settings if needed. + if ( isset( $service_data['lazyload_type'] ) && 'all' === $service_data['lazyload_type'] ) { + $service_data['lazyload_type'] = 'fixed'; + $service_data['skip_lazyload_images'] = 0; + } + return [ 'strings' => $this->get_dashboard_strings(), 'assets_url' => OPTML_URL . 'assets/', From eddfd1a34abd5a0d206786802fa95c9a6fd5d314 Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Tue, 23 Sep 2025 17:44:40 +0300 Subject: [PATCH 08/13] chore: refactor --- .../parts/components/Miscellaneous.js | 37 ++++ .../parts/connected/settings/Lazyload.js | 195 +++++------------- inc/admin.php | 6 +- 3 files changed, 91 insertions(+), 147 deletions(-) create mode 100644 assets/src/dashboard/parts/components/Miscellaneous.js diff --git a/assets/src/dashboard/parts/components/Miscellaneous.js b/assets/src/dashboard/parts/components/Miscellaneous.js new file mode 100644 index 000000000..e515ebc3e --- /dev/null +++ b/assets/src/dashboard/parts/components/Miscellaneous.js @@ -0,0 +1,37 @@ +import classNames from 'classnames'; + +export const TextWithWarningBadge = ({ text, badgeLabel }) => { + return ( + <> + {text} + + {badgeLabel} + + + ); +}; + +const DescriptionTag = ({ text, showAsDisabled }) => ( + + {text} + +); + +export const DescriptionWithTags = ({ text, tags }) => ( + <> + {text} +
    + {tags.map( ({ text, disabled }) => ( + + ) )} +
    + +); diff --git a/assets/src/dashboard/parts/connected/settings/Lazyload.js b/assets/src/dashboard/parts/connected/settings/Lazyload.js index 9618c8cab..7e68e9756 100644 --- a/assets/src/dashboard/parts/connected/settings/Lazyload.js +++ b/assets/src/dashboard/parts/connected/settings/Lazyload.js @@ -35,6 +35,12 @@ import { } from '../../components/GroupSettingsContainer'; import Notice from '../../components/Notice'; import Modal from '../../components/Modal'; +import { + DescriptionWithTags, + TextWithWarningBadge +} from '../../components/Miscellaneous'; + +const { options_strings } = optimoleDashboardApp.strings; const DISABLE_OPTION_MODAL_TYPE = { scale: 'image-scaling', @@ -97,9 +103,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { const supportPrefilledContactFormUrl = useMemo( () => { const contactUrl = new URL( window.optimoleDashboardApp.report_issue_url ); - const { - contact_support - } = window.optimoleDashboardApp.strings; + const { contact_support } = window.optimoleDashboardApp.strings; let subject = contact_support.disable_lazy_load_scaling; if ( DISABLE_OPTION_MODAL_TYPE.scale === showModal ) { @@ -110,10 +114,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { contactUrl.searchParams.set( 'contact_subject', - sprintf( - contact_support.title_prefix, - subject - ) + sprintf( contact_support.title_prefix, subject ) ); return contactUrl; @@ -164,56 +165,12 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { [ settings?.lazyload_type ] ); - const NotRecommendedWarning = useCallback( ( props ) => { - return ( - <> - {props.label} - - {optimoleDashboardApp.strings.options_strings.not_recommended} - - - ); - }, []); - - const Tag = useCallback( - ({ text, disabled }) => ( - - {text} - - ), - [] - ); - - const DescriptionWithTags = useCallback( - ({ text, tags }) => { - return ( - <> - {text} -
    - {tags.map( ({ text, disabled }) => ( - - ) )} -
    - - ); - }, - [ Tag ] - ); - if ( ! isLazyLoadEnabled ) { return ( <> { return ( <> { icon="warning" variant="warning" labels={{ - title: - optimoleDashboardApp.strings.options_strings - .performance_impact_alert_title, + title: options_strings.performance_impact_alert_title, description: 'scale' === showModal ? - optimoleDashboardApp.strings.options_strings - .performance_impact_alert_scale_desc : - optimoleDashboardApp.strings.options_strings - .performance_impact_alert_lazy_desc, - action: - optimoleDashboardApp.strings.options_strings - .performance_impact_alert_action_label, + options_strings.performance_impact_alert_scale_desc : + options_strings.performance_impact_alert_lazy_desc, + action: options_strings.performance_impact_alert_action_label, secondaryAction: - optimoleDashboardApp.strings.options_strings - .performance_impact_alert_secondary_action_label + options_strings.performance_impact_alert_secondary_action_label }} onRequestClose={() => setShowModal( false )} onConfirm={() => { @@ -285,36 +235,26 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => {
    @@ -323,23 +263,23 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { }, { title: ( - + ), description: ( { - { - optimoleDashboardApp.strings.options_strings - .lazyload_behaviour_title - }{' '} - ({optimoleDashboardApp.strings.options_strings.global_option}) + {options_strings.lazyload_behaviour_title} ( + {options_strings.global_option}) { toggleLoadingBehavior( value, 'fixed' ); @@ -397,10 +331,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { { toggleLoadingBehavior( value, 'viewport' ); @@ -413,25 +344,19 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { )} - {optimoleDashboardApp.strings.options_strings.visual_settings} + {options_strings.visual_settings}
    toggleOption( 'lazyload_placeholder', value ) @@ -476,7 +401,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { setColor( '' ); }} > - {optimoleDashboardApp.strings.options_strings.clear} + {options_strings.clear} )} @@ -487,8 +412,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { <noscript> } @@ -506,18 +430,15 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { )}
    { - {optimoleDashboardApp.strings.options_strings.extended_features} + {options_strings.extended_features} (

    )} @@ -563,32 +479,23 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { updateValue( 'watchers', value )} - help={ - optimoleDashboardApp.strings.options_strings.watch_desc_lazyload - } + help={options_strings.watch_desc_lazyload} /> )} (

    )} diff --git a/inc/admin.php b/inc/admin.php index 0cfc96741..33869da83 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1967,7 +1967,7 @@ private function get_dashboard_strings() { 'low_q_title' => __( 'Low', 'optimole-wp' ), 'medium_q_title' => __( 'Medium', 'optimole-wp' ), 'no_images_found' => __( 'You dont have any images in your Media Library. Add one and check how the Optimole will perform.', 'optimole-wp' ), - 'native_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable to use the browser\'s built-in lazy loading feature. Enabling this will disable the auto scale feature, meaning images will not be automatically resized to fit the screen dimensions. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), + 'native_desc' => __( 'Uses the browser\'s build-in "lazy" behavior', 'optimole-wp' ), 'option_saved' => __( 'Option saved.', 'optimole-wp' ), 'ml_quality_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Optimole ML algorithms will predict the optimal image quality to get the smallest possible size with minimum perceived quality losses. When disabled, you can control the quality manually. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), 'quality_desc' => __( 'Lower image quality might boost your loading speed by lowering the size. However, the low image quality may negatively impact the visual appearance of the images. Try experimenting with the setting, then click the View sample image link to see what option works best for you.', 'optimole-wp' ), @@ -1990,7 +1990,7 @@ private function get_dashboard_strings() { 'toggle_ab_item' => __( 'Admin bar status', 'optimole-wp' ), 'toggle_lazyload' => __( 'Enable Lazy Loading & Scaling', 'optimole-wp' ), 'toggle_scale' => __( 'Smart Image Scaling', 'optimole-wp' ), - 'toggle_native' => __( 'Browser Native Lazy Load', 'optimole-wp' ), + 'toggle_native' => __( 'Native Browser Loading', 'optimole-wp' ), 'on_toggle' => __( 'On', 'optimole-wp' ), 'off_toggle' => __( 'Off', 'optimole-wp' ), 'view_sample_image' => __( 'View sample image', 'optimole-wp' ), @@ -2098,7 +2098,7 @@ private function get_dashboard_strings() { 'performance_impact_alert_lazy_desc' => __( 'Disabling Lazy Load may significantly impact your site\'s loading speed and Core Web Vitals scores.', 'optimole-wp' ), 'performance_impact_alert_scale_desc' => __( 'Disabling Image Scaling may significantly impact your site\'s loading speed and Core Web Vitals scores.', 'optimole-wp' ), 'performance_impact_alert_action_label' => __( 'Continue with disabling', 'optimole-wp' ), - 'performance_impact_alert_secondary_action_label' => __( 'Keep enabled', 'optimole-wp' ), + 'performance_impact_alert_secondary_action_label' => __( 'Keep enabled and get help', 'optimole-wp' ), ], 'help' => [ 'section_one_title' => __( 'Help and Support', 'optimole-wp' ), From f05c4cdf6c4f935d826007f3649f6561d821a9e1 Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Tue, 23 Sep 2025 17:59:11 +0300 Subject: [PATCH 09/13] fix: migration --- inc/admin.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/inc/admin.php b/inc/admin.php index 33869da83..738f491f2 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1345,10 +1345,11 @@ private function localize_dashboard_app() { ]; $lang_code = isset( $available_languages[ $language ] ) ? 'de' : 'en'; + $site_settings = $this->settings->get_site_settings(); // Migrate settings if needed. - if ( isset( $service_data['lazyload_type'] ) && 'all' === $service_data['lazyload_type'] ) { - $service_data['lazyload_type'] = 'fixed'; - $service_data['skip_lazyload_images'] = 0; + if ( isset( $site_settings['lazyload_type'] ) && 'all' === $site_settings['lazyload_type'] ) { + $site_settings['lazyload_type'] = 'fixed'; + $site_settings['skip_lazyload_images'] = 0; } return [ @@ -1368,7 +1369,7 @@ private function localize_dashboard_app() { 'current_user' => [ 'email' => $user->user_email, ], - 'site_settings' => $this->settings->get_site_settings(), + 'site_settings' => $site_settings, 'offload_limit' => $this->settings->get( 'offload_limit' ), 'home_url' => home_url(), 'optimoleHome' => tsdk_translate_link( 'https://optimole.com/' ), From f158c045690d7ab4d0cb749ed69e5e05f320000e Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Wed, 24 Sep 2025 10:26:57 +0300 Subject: [PATCH 10/13] chore: exclude global type declaration since the current parser can not read it --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index ebd30d933..7745aad3b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,7 +11,7 @@ "ecmaVersion": 2021, "sourceType": "module" }, - "ignorePatterns": [ "node_modules", "assets/js" ], + "ignorePatterns": [ "node_modules", "assets/js", "**/*.d.ts" ], "rules": { "camelcase" : "off", "indent": [ From 07c7ed14ae8d3fdc61ce8d27c15d7d10d8ddf051 Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Fri, 26 Sep 2025 09:58:43 +0300 Subject: [PATCH 11/13] chore: clean up --- .../parts/connected/settings/Resize.js | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/assets/src/dashboard/parts/connected/settings/Resize.js b/assets/src/dashboard/parts/connected/settings/Resize.js index b5800878a..62989e665 100644 --- a/assets/src/dashboard/parts/connected/settings/Resize.js +++ b/assets/src/dashboard/parts/connected/settings/Resize.js @@ -17,7 +17,6 @@ import { import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; -import Notice from '../../components/Notice'; const Resize = ({ settings, @@ -38,8 +37,6 @@ const Resize = ({ const isSmartResizeEnabled = 'disabled' !== settings[ 'resize_smart' ]; const isLimitDimensionsEnabled = 'disabled' !== settings[ 'limit_dimensions' ]; - const isLazyloadEnabled = 'disabled' !== settings.lazyload; - const isScaleEnabled = 'disabled' === settings.scale; const updateOption = ( option, value ) => { setCanSave( true ); const data = { ...settings }; @@ -109,25 +106,6 @@ const Resize = ({


    - - {isLazyloadEnabled && ( - <> -

    } - checked={ isScaleEnabled } - disabled={ isLoading } - className={ classnames( - { - 'is-disabled': isLoading - } - ) } - onChange={ value => updateOption( 'scale', ! value ) } - /> -


    - ) } - -

    } From 142f63b572d8d24ba7dec5a63dd8321765bbb1bb Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Tue, 30 Sep 2025 17:27:09 +0300 Subject: [PATCH 12/13] chore: fix label names --- inc/admin.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inc/admin.php b/inc/admin.php index 738f491f2..3c1f9d2a7 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1837,7 +1837,7 @@ private function get_dashboard_strings() { '' ), - 'lazyload_behaviour_title' => __( 'Loading Behaviour', 'optimole-wp' ), + 'lazyload_behaviour_title' => __( 'Loading Method', 'optimole-wp' ), 'lazyload_behaviour_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Choose how Optimole will handle lazy loading for images on your website. %1$sLearn more%2$s', 'optimole-wp' ), @@ -1848,7 +1848,7 @@ private function get_dashboard_strings() { 'lazyload_behaviour_all_desc' => __( 'All images will use lazy loading regardless of position.', 'optimole-wp' ), 'lazyload_behaviour_viewport' => __( 'Enable viewport-based loading', 'optimole-wp' ), 'lazyload_behaviour_viewport_desc' => __( 'Automatically detects and immediately loads images visible in the initial viewport. Detection is done with a lightweight client-side script that identifies what\'s visible on each user\'s screen. All other images will lazy load.', 'optimole-wp' ), - 'lazyload_behaviour_fixed' => __( 'Skip lazy Loading for first images', 'optimole-wp' ), + 'lazyload_behaviour_fixed' => __( 'Skip lazy loading for first images', 'optimole-wp' ), 'lazyload_behaviour_fixed_desc' => __( 'Indicate how many images at the top of each page should bypass lazy loading, ensuring they\'re instantly visible.', 'optimole-wp' ), 'enable_lazyload_placeholder_title' => __( 'Enable generic placeholder color', 'optimole-wp' ), 'enable_network_opt_desc' => sprintf( @@ -2000,18 +2000,18 @@ private function get_dashboard_strings() { 'watch_placeholder_lazyload_example' => sprintf( __( 'Example: %s', 'optimole-wp' ), '.hero-bg, #banner-image, .lazy-bg, .footer-bg' ), 'watch_desc_lazyload' => __( 'If everything is covered out of the box, leave this empty.', 'optimole-wp' ), 'smart_loading_title' => __( 'Smart Loading', 'optimole-wp' ), - 'smart_loading_desc' => __( 'JavaScript-drive with advanced controls', 'optimole-wp' ), + 'smart_loading_desc' => __( 'JavaScript-driven with advanced controls', 'optimole-wp' ), 'browser_native_lazy' => __( 'Uses the browser\'s built-in lazy loading feature', 'optimole-wp' ), // translators: viewport is the visible area of a web page on a display device. 'viewport_detection' => __( 'Viewport detection', 'optimole-wp' ), // translators: set the colors for the placeholder image. - 'placeholders_color' => __( 'Placeholder', 'optimole-wp' ), + 'placeholders_color' => __( 'Placeholders', 'optimole-wp' ), // translators: it can handle many images without slowing down the site. 'auto_scaling' => __( 'Auto Scaling', 'optimole-wp' ), // translators: it uses the browser's built-in lazy loading feature without any additional scripts. 'lightweight_native' => __( 'Lightweight', 'optimole-wp' ), - 'watch_title_lazyload' => __( 'Extend CSS Background Lazy Loading', 'optimole-wp' ), + 'watch_title_lazyload' => __( 'Extend CSS Background Images', 'optimole-wp' ), 'width_field' => __( 'Width', 'optimole-wp' ), 'crop' => __( 'crop', 'optimole-wp' ), 'toggle_cdn' => __( 'Serve CSS & JS Through Optimole', 'optimole-wp' ), From 8c9de99738f5888911d3a2df6a9d885015fc73c5 Mon Sep 17 00:00:00 2001 From: Soare Robert-Daniel Date: Wed, 1 Oct 2025 15:27:55 +0300 Subject: [PATCH 13/13] chore: added learn more --- assets/src/dashboard/parts/connected/settings/Lazyload.js | 4 ++-- inc/admin.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/src/dashboard/parts/connected/settings/Lazyload.js b/assets/src/dashboard/parts/connected/settings/Lazyload.js index 7e68e9756..5d8300651 100644 --- a/assets/src/dashboard/parts/connected/settings/Lazyload.js +++ b/assets/src/dashboard/parts/connected/settings/Lazyload.js @@ -170,7 +170,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { <>

    } checked={isLazyLoadEnabled} disabled={isLoading} className={classnames({ @@ -186,7 +186,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => { <>

    } checked={isLazyLoadEnabled} disabled={isLoading} className={classnames({ diff --git a/inc/admin.php b/inc/admin.php index 3c1f9d2a7..b5cabe68a 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -1962,7 +1962,7 @@ private function get_dashboard_strings() { 'high_q_title' => __( 'High', 'optimole-wp' ), 'image_1_label' => __( 'Original', 'optimole-wp' ), 'image_2_label' => __( 'Optimized', 'optimole-wp' ), - 'lazyload_desc' => __( 'Images load only when they\'re about to enter the viewport', 'optimole-wp' ), + 'lazyload_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Images load only when they\'re about to enter the viewport. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), 'filter_length_error' => __( 'The filter should be at least 3 characters long.', 'optimole-wp' ), 'scale_desc' => __( 'Automatically resize the images based on device and viewport', 'optimole-wp' ), 'low_q_title' => __( 'Low', 'optimole-wp' ),