array(), 'strong' => array(), 'em' => array(), 'br' => array(), 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), ), ); } /** * Sections per tab, in display order. * * @return array tab => list of [ id, title, intro, keys, collapsible ]. */ public static function sections() { return array( 'general' => array( array( 'id' => 'activation', 'title' => __( 'Activation', 'ajax-loading' ), 'intro' => __( 'Turn AJAX navigation on, try it in test mode, and choose the devices that use it.', 'ajax-loading' ), 'keys' => array( 'enable_plugin', 'visibility', 'enable_desktop', 'enable_mobile', 'mobile_breakpoint' ), ), array( 'id' => 'content', 'title' => __( 'Content regions', 'ajax-loading' ), 'intro' => __( 'Which part of the page is replaced when a visitor follows a link. Everything else (header, menus, footer) stays in place.', 'ajax-loading' ), 'keys' => array( 'navigation_mode', 'content_selectors', 'header_selector', 'header_mode', 'footer_selector', 'footer_mode', 'handle_404' ), ), array( 'id' => 'forms', 'title' => __( 'Forms', 'ajax-loading' ), 'intro' => __( 'Search and filter forms that use the GET method can be submitted without a full reload. POST forms are never handled.', 'ajax-loading' ), 'keys' => array( 'forms_search', 'form_selectors' ), ), array( 'id' => 'advanced', 'title' => __( 'Advanced: scripts and compatibility', 'ajax-loading' ), 'intro' => __( 'The defaults suit most sites. Change these only when a theme or plugin needs it.', 'ajax-loading' ), 'keys' => array( 'execute_content_scripts', 'run_new_inline_scripts', 'refresh_localized_data', 'dispatch_dom_events', 'remove_stale_styles', 'legacy_jquery', 'disable_emojis' ), 'collapsible' => true, ), ), 'loading' => array( array( 'id' => 'indicator', 'title' => __( 'Loading indicator', 'ajax-loading' ), 'intro' => __( 'Shown while the next page is loading. Fast navigations finish before the indicator appears.', 'ajax-loading' ), 'keys' => array( 'loader_type', 'mobile_loader_type', 'loader_color', 'loader_bg', 'loader_text', 'loader_logo_id', 'loader_size', 'bar_height', 'bar_position', 'loader_position', 'overlay_opacity', 'loader_speed', 'loader_zindex' ), ), array( 'id' => 'timing', 'title' => __( 'Indicator timing', 'ajax-loading' ), 'intro' => '', 'keys' => array( 'loader_delay', 'loader_min_duration' ), ), array( 'id' => 'transitions', 'title' => __( 'Page transitions', 'ajax-loading' ), 'intro' => __( 'Animations played when the old content leaves and the new content enters. Only opacity and transform are animated, so they stay smooth on slow devices.', 'ajax-loading' ), 'keys' => array( 'transition_out', 'transition_in', 'transition_duration', 'transition_easing', 'transition_header_footer', 'mobile_transition', 'respect_reduced_motion' ), ), array( 'id' => 'after', 'title' => __( 'After navigation', 'ajax-loading' ), 'intro' => __( 'Scrolling, keyboard focus and screen reader announcements once the new page is shown.', 'ajax-loading' ), 'keys' => array( 'scroll_behavior', 'focus_target', 'announce_navigation' ), ), ), 'performance' => array( array( 'id' => 'prefetch', 'title' => __( 'Prefetching', 'ajax-loading' ), 'intro' => __( 'Start loading a page as soon as a visitor shows intent, so it opens almost instantly. Logged-in visitors and personalised pages (cart, comments) are never prefetched.', 'ajax-loading' ), 'keys' => array( 'prefetch_enabled', 'prefetch_triggers', 'prefetch_hover_delay', 'prefetch_max_concurrent', 'prefetch_viewport_limit', 'prefetch_exclude', 'network_aware' ), ), array( 'id' => 'cache', 'title' => __( 'In-browser page cache', 'ajax-loading' ), 'intro' => __( 'Keeps recently visited pages in the visitor\'s browser memory. Nothing is stored on your server and the cache ends when the tab is closed.', 'ajax-loading' ), 'keys' => array( 'cache_enabled', 'cache_max_pages', 'cache_ttl', 'cache_max_kb' ), ), array( 'id' => 'requests', 'title' => __( 'Requests', 'ajax-loading' ), 'intro' => '', 'keys' => array( 'request_timeout', 'disable_core_speculation' ), ), array( 'id' => 'state', 'title' => __( 'Page state', 'ajax-loading' ), 'intro' => __( 'What is restored or updated so AJAX pages behave like normal page loads.', 'ajax-loading' ), 'keys' => array( 'preserve_scroll', 'preserve_form_state', 'sync_menu_state' ), ), ), 'seo' => array( array( 'id' => 'metadata', 'title' => __( 'Search engines and social sharing', 'ajax-loading' ), 'intro' => __( 'The page title is always updated. These options keep the rest of the
in sync, so copied links, browser extensions and SEO tools see the right data.', 'ajax-loading' ), 'keys' => array( 'sync_metadata', 'sync_canonical', 'sync_structured_data' ), ), array( 'id' => 'analytics', 'title' => __( 'Analytics', 'ajax-loading' ), 'intro' => __( 'Make sure every AJAX navigation is counted as a page view exactly once.', 'ajax-loading' ), 'keys' => array( 'analytics_mode', 'analytics_timing', 'gtm_event_name', 'analytics_require_consent', 'analytics_callback' ), ), array( 'id' => 'integrations', 'title' => __( 'Integrations', 'ajax-loading' ), 'intro' => __( 'Built-in adapters re-initialise popular plugins after each navigation. "Auto" uses the adapter, "Exclude" makes pages that use the integration load normally, "Off" ignores it.', 'ajax-loading' ), 'keys' => array( 'integrations' ), ), ), 'exclusions' => array( array( 'id' => 'rules', 'title' => __( 'Your exclusions', 'ajax-loading' ), 'intro' => __( 'Links that match any of these rules load normally (full page load).', 'ajax-loading' ), 'keys' => array( 'exclude_urls', 'exclude_selectors', 'exclude_post_types' ), ), array( 'id' => 'builtin', 'title' => __( 'Protected areas', 'ajax-loading' ), 'intro' => '', 'keys' => array( 'exclude_woocommerce', 'exclude_membership', 'interactive_blocks' ), ), ), 'tools' => array( array( 'id' => 'safety', 'title' => __( 'Safety', 'ajax-loading' ), 'intro' => __( 'Whenever AJAX navigation cannot handle a page, the browser simply loads it normally. These options add extra protection.', 'ajax-loading' ), 'keys' => array( 'safe_mode', 'auto_disable', 'auto_disable_threshold', 'auto_disable_minutes' ), ), array( 'id' => 'debugging', 'title' => __( 'Debugging', 'ajax-loading' ), 'intro' => '', 'keys' => array( 'debug_mode', 'error_logging' ), ), ), ); } /** * Human metadata per setting key. * * Keys: label, desc, control (radio|select|media), choices (value => label), * choice_desc (value => text), unit, placeholder, maxlength, pattern, rows, * show_if (list of { fields: [], in|not_in: [] }; all conditions must match). * * @return array */ public static function meta() { if ( null !== self::$meta ) { return self::$meta; } $ms = _x( 'ms', 'milliseconds unit', 'ajax-loading' ); $px = _x( 'px', 'pixels unit', 'ajax-loading' ); $seconds = _x( 'seconds', 'time unit', 'ajax-loading' ); $minutes = _x( 'minutes', 'time unit', 'ajax-loading' ); $loader_choices = array( 'bar' => __( 'Progress bar', 'ajax-loading' ), 'spinner' => __( 'Spinner', 'ajax-loading' ), 'overlay' => __( 'Overlay', 'ajax-loading' ), 'text' => __( 'Text', 'ajax-loading' ), 'logo' => __( 'Your logo', 'ajax-loading' ), 'skeleton' => __( 'Skeleton placeholder', 'ajax-loading' ), 'none' => __( 'None', 'ajax-loading' ), ); $transition_choices = array( 'fade' => __( 'Fade', 'ajax-loading' ), 'fade-through' => __( 'Fade through', 'ajax-loading' ), 'slide-left' => __( 'Slide left', 'ajax-loading' ), 'slide-right' => __( 'Slide right', 'ajax-loading' ), 'slide-up' => __( 'Slide up', 'ajax-loading' ), 'slide-down' => __( 'Slide down', 'ajax-loading' ), 'scale' => __( 'Scale', 'ajax-loading' ), 'smooth' => __( 'Smooth (fade and lift)', 'ajax-loading' ), 'none' => __( 'None', 'ajax-loading' ), ); $any_loader = array( array( 'fields' => array( 'loader_type', 'mobile_loader_type' ), 'not_in' => array( 'none', 'inherit' ), ), ); $loader_is = static function ( array $types ) { return array( array( 'fields' => array( 'loader_type', 'mobile_loader_type' ), 'in' => $types, ), ); }; $prefetch_on = array( array( 'fields' => array( 'prefetch_enabled' ), 'in' => array( '1' ), ), ); self::$meta = array( // General. 'enable_plugin' => array( 'label' => __( 'Enable AJAX navigation', 'ajax-loading' ), 'desc' => __( 'Master switch. When off, your site loads every page normally.', 'ajax-loading' ), ), 'visibility' => array( 'label' => __( 'Who gets AJAX navigation', 'ajax-loading' ), 'control' => 'radio', 'choices' => array( 'everyone' => __( 'Everyone', 'ajax-loading' ), 'admins' => __( 'Administrators only (test mode)', 'ajax-loading' ), ), 'choice_desc' => array( 'admins' => __( 'Try the plugin on your live site first: visitors keep normal page loads until you switch to everyone.', 'ajax-loading' ), ), ), 'enable_desktop' => array( 'label' => __( 'Desktop devices', 'ajax-loading' ), 'desc' => __( 'Use AJAX navigation on screens wider than the mobile breakpoint.', 'ajax-loading' ), ), 'enable_mobile' => array( 'label' => __( 'Mobile devices', 'ajax-loading' ), 'desc' => __( 'Use AJAX navigation on screens up to the mobile breakpoint.', 'ajax-loading' ), ), 'mobile_breakpoint' => array( 'label' => __( 'Mobile breakpoint', 'ajax-loading' ), 'desc' => __( 'Screens this wide or narrower count as mobile. Also used by the mobile indicator and animation options.', 'ajax-loading' ), 'unit' => $px, ), 'navigation_mode' => array( 'label' => __( 'Content detection', 'ajax-loading' ), 'control' => 'radio', 'choices' => array( 'auto' => __( 'Automatic (recommended)', 'ajax-loading' ), 'custom' => __( 'Custom selectors', 'ajax-loading' ), 'body' => __( 'Whole page body', 'ajax-loading' ), ), 'choice_desc' => array( 'auto' => __( 'Finds your theme\'s main content area on both pages and only swaps it. If the two pages use different layouts, the page loads normally.', 'ajax-loading' ), 'custom' => __( 'Swaps exactly the regions listed in "Content selectors". Each must exist on both pages.', 'ajax-loading' ), 'body' => __( 'Replaces everything except the admin bar. Heavier, and header scripts may need to re-initialise.', 'ajax-loading' ), ), ), 'content_selectors' => array( 'label' => __( 'Content selectors', 'ajax-loading' ), 'desc' => sprintf( /* translators: 1: example selector, 2: example selector. */ __( 'One CSS selector per line, for example %1$s or %2$s. In Custom mode these regions are replaced; in Automatic mode they are tried before the built-in candidates.', 'ajax-loading' ), '#content',
'main'
),
'placeholder' => '#content',
'rows' => 3,
),
'header_selector' => array(
'label' => __( 'Header selector', 'ajax-loading' ),
'desc' => __( 'Leave empty to detect the site header automatically.', 'ajax-loading' ),
'placeholder' => __( 'Auto-detect', 'ajax-loading' ),
'maxlength' => 500,
),
'header_mode' => array(
'label' => __( 'Header', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'preserve' => __( 'Keep it (recommended)', 'ajax-loading' ),
'replace' => __( 'Replace it on every navigation', 'ajax-loading' ),
),
'desc' => __( 'Replace the header only when it shows page-specific content, such as a different hero image per page.', 'ajax-loading' ),
),
'footer_selector' => array(
'label' => __( 'Footer selector', 'ajax-loading' ),
'desc' => __( 'Leave empty to detect the site footer automatically.', 'ajax-loading' ),
'placeholder' => __( 'Auto-detect', 'ajax-loading' ),
'maxlength' => 500,
),
'footer_mode' => array(
'label' => __( 'Footer', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'preserve' => __( 'Keep it (recommended)', 'ajax-loading' ),
'replace' => __( 'Replace it on every navigation', 'ajax-loading' ),
),
'desc' => __( 'Replace the footer only when it shows page-specific content, such as widgets that differ from page to page.', 'ajax-loading' ),
),
'handle_404' => array(
'label' => __( 'Load "not found" pages with AJAX', 'ajax-loading' ),
'desc' => __( 'Show your theme\'s 404 page without a full reload. When off, broken links load normally.', 'ajax-loading' ),
),
'forms_search' => array(
'label' => __( 'AJAX search forms', 'ajax-loading' ),
'desc' => __( 'Submit WordPress search forms (theme search form and Search block) without a full reload.', 'ajax-loading' ),
),
'form_selectors' => array(
'label' => __( 'Additional GET forms', 'ajax-loading' ),
'desc' => __( 'CSS selectors of other GET forms, such as product filters, one per line. Forms with file uploads are never handled.', 'ajax-loading' ),
'placeholder' => '.filter-form',
'rows' => 3,
),
'execute_content_scripts' => array(
'label' => __( 'Run scripts inside new content', 'ajax-loading' ),
'desc' => __( 'Execute inline and external scripts that are part of the replaced content, like a normal page load does.', 'ajax-loading' ),
),
'run_new_inline_scripts' => array(
'label' => __( 'Run new inline scripts', 'ajax-loading' ),
'desc' => __( 'Execute inline scripts outside the content area that only exist on the destination page. Analytics snippets are never re-run.', 'ajax-loading' ),
),
'refresh_localized_data' => array(
'label' => sprintf(
/* translators: %s: script id suffix. */
__( 'Refresh changed %s data', 'ajax-loading' ),
'-js-extra'
),
'desc' => __( 'Re-run WordPress script data blocks that differ on the new page, so plugins read the new page\'s settings.', 'ajax-loading' ),
),
'dispatch_dom_events' => array(
'label' => __( 'Fire page-load events again', 'ajax-loading' ),
'desc' => sprintf(
/* translators: 1: DOMContentLoaded, 2: load. */
__( 'Dispatch synthetic %1$s and %2$s events after each navigation for old scripts that only initialise on page load. Some scripts may then initialise twice; enable only when needed.', 'ajax-loading' ),
'DOMContentLoaded',
'load'
),
),
'remove_stale_styles' => array(
'label' => __( 'Remove unused stylesheets', 'ajax-loading' ),
'desc' => __( 'Remove stylesheets that the new page does not use. Keeps long sessions lean, but a stylesheet needed again must be downloaded again.', 'ajax-loading' ),
),
'legacy_jquery' => array(
'label' => __( 'Load jQuery for custom callbacks', 'ajax-loading' ),
'desc' => sprintf(
/* translators: %s: callback name. */
__( 'Version 1.x always loaded jQuery. Keep this on if your own %s code uses jQuery.', 'ajax-loading' ),
'wp_ajax_load_complete'
),
),
'disable_emojis' => array(
'label' => __( 'Disable WordPress emoji scripts', 'ajax-loading' ),
'desc' => __( 'Removes the emoji detection script and styles from the front end, as version 1.x did. Modern browsers show emoji natively.', 'ajax-loading' ),
),
// Loading experience.
'loader_type' => array(
'label' => __( 'Indicator', 'ajax-loading' ),
'control' => 'select',
'choices' => $loader_choices,
'desc' => __( 'Shown while the next page loads.', 'ajax-loading' ),
),
'mobile_loader_type' => array(
'label' => __( 'Indicator on mobile', 'ajax-loading' ),
'control' => 'select',
'choices' => array( 'inherit' => __( 'Same as desktop', 'ajax-loading' ) ) + $loader_choices,
'desc' => __( 'Indicator used on screens up to the mobile breakpoint.', 'ajax-loading' ),
),
'loader_color' => array(
'label' => __( 'Indicator colour', 'ajax-loading' ),
'desc' => __( 'Main colour of the progress bar, the spinner and the loading text.', 'ajax-loading' ),
'show_if' => $loader_is( array( 'bar', 'spinner', 'overlay', 'text', 'logo' ) ),
),
'loader_bg' => array(
'label' => __( 'Background colour', 'ajax-loading' ),
'desc' => __( 'Used by the overlay, the skeleton placeholder and the plate behind the spinner, logo and text.', 'ajax-loading' ),
'show_if' => $loader_is( array( 'spinner', 'overlay', 'text', 'logo', 'skeleton' ) ),
),
'loader_text' => array(
'label' => __( 'Loading text', 'ajax-loading' ),
'desc' => __( 'Leave empty to use the translated default.', 'ajax-loading' ),
'placeholder' => __( 'Loading…', 'ajax-loading' ),
'maxlength' => 100,
'show_if' => $loader_is( array( 'text', 'overlay' ) ),
),
'loader_logo_id' => array(
'label' => __( 'Logo image', 'ajax-loading' ),
'desc' => __( 'Image shown by the "Your logo" indicator. Square images of about 200 pixels work best.', 'ajax-loading' ),
'control' => 'media',
'show_if' => $loader_is( array( 'logo' ) ),
),
'loader_size' => array(
'label' => __( 'Indicator size', 'ajax-loading' ),
'desc' => __( 'Size of the spinner or logo.', 'ajax-loading' ),
'unit' => $px,
'show_if' => $loader_is( array( 'spinner', 'overlay', 'logo' ) ),
),
'bar_height' => array(
'label' => __( 'Bar height', 'ajax-loading' ),
'desc' => __( 'Thickness of the progress bar.', 'ajax-loading' ),
'unit' => $px,
'show_if' => $loader_is( array( 'bar' ) ),
),
'bar_position' => array(
'label' => __( 'Bar position', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'top' => __( 'Top of the screen', 'ajax-loading' ),
'bottom' => __( 'Bottom of the screen', 'ajax-loading' ),
),
'desc' => __( 'Edge of the screen the progress bar runs along.', 'ajax-loading' ),
'show_if' => $loader_is( array( 'bar' ) ),
),
'loader_position' => array(
'label' => __( 'Indicator position', 'ajax-loading' ),
'control' => 'select',
'choices' => array(
'center' => __( 'Centre', 'ajax-loading' ),
'top-left' => __( 'Top left', 'ajax-loading' ),
'top-right' => __( 'Top right', 'ajax-loading' ),
'bottom-left' => __( 'Bottom left', 'ajax-loading' ),
'bottom-right' => __( 'Bottom right', 'ajax-loading' ),
),
'desc' => __( 'Where the spinner, text or logo appears.', 'ajax-loading' ),
'show_if' => $loader_is( array( 'spinner', 'text', 'logo' ) ),
),
'overlay_opacity' => array(
'label' => __( 'Overlay opacity', 'ajax-loading' ),
'desc' => __( '0 is fully transparent, 1 is solid.', 'ajax-loading' ),
'show_if' => $loader_is( array( 'overlay' ) ),
),
'loader_speed' => array(
'label' => __( 'Animation speed', 'ajax-loading' ),
'desc' => __( 'Length of one animation cycle. Lower is faster.', 'ajax-loading' ),
'unit' => $ms,
'show_if' => $any_loader,
),
'loader_zindex' => array(
'label' => __( 'Stacking order (z-index)', 'ajax-loading' ),
'desc' => __( 'Raise this if the indicator is hidden behind a sticky header or popup.', 'ajax-loading' ),
'show_if' => $any_loader,
),
'loader_delay' => array(
'label' => __( 'Show the indicator after', 'ajax-loading' ),
'desc' => __( 'Navigations that finish sooner never show an indicator, so fast pages do not flicker.', 'ajax-loading' ),
'unit' => $ms,
'show_if' => $any_loader,
),
'loader_min_duration' => array(
'label' => __( 'Minimum display time', 'ajax-loading' ),
'desc' => __( 'Once visible, the indicator stays at least this long.', 'ajax-loading' ),
'unit' => $ms,
'show_if' => $any_loader,
),
'transition_out' => array(
'label' => __( 'Leave animation', 'ajax-loading' ),
'control' => 'select',
'choices' => $transition_choices,
'desc' => __( 'Played on the old content while the next page loads.', 'ajax-loading' ),
),
'transition_in' => array(
'label' => __( 'Enter animation', 'ajax-loading' ),
'control' => 'select',
'choices' => $transition_choices,
'desc' => __( 'Played on the new content.', 'ajax-loading' ),
),
'transition_duration' => array(
'label' => __( 'Animation duration', 'ajax-loading' ),
'desc' => __( 'Length of each leave and enter animation.', 'ajax-loading' ),
'unit' => $ms,
'show_if' => array(
array(
'fields' => array( 'transition_out', 'transition_in', 'mobile_transition' ),
'not_in' => array( 'none', 'inherit' ),
),
),
),
'transition_easing' => array(
'label' => __( 'Easing', 'ajax-loading' ),
'control' => 'select',
'choices' => array(
'ease' => __( 'Ease', 'ajax-loading' ),
'ease-in-out' => __( 'Ease in and out', 'ajax-loading' ),
'ease-out' => __( 'Ease out', 'ajax-loading' ),
'linear' => __( 'Linear', 'ajax-loading' ),
'snappy' => __( 'Snappy', 'ajax-loading' ),
),
'desc' => __( 'Speed curve of the animations.', 'ajax-loading' ),
'show_if' => array(
array(
'fields' => array( 'transition_out', 'transition_in', 'mobile_transition' ),
'not_in' => array( 'none', 'inherit' ),
),
),
),
'transition_header_footer' => array(
'label' => __( 'Animate header and footer', 'ajax-loading' ),
'desc' => __( 'Also animate the header and footer when they are set to "Replace" on the General tab.', 'ajax-loading' ),
),
'mobile_transition' => array(
'label' => __( 'Animation on mobile', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'inherit' => __( 'Same as desktop', 'ajax-loading' ),
'fade' => __( 'Fade only', 'ajax-loading' ),
'none' => __( 'None', 'ajax-loading' ),
),
'desc' => __( 'Animation used on screens up to the mobile breakpoint.', 'ajax-loading' ),
),
'respect_reduced_motion' => array(
'label' => __( 'Respect reduced motion', 'ajax-loading' ),
'desc' => __( 'Skip animations for visitors whose device asks for reduced motion. Strongly recommended for accessibility.', 'ajax-loading' ),
),
'scroll_behavior' => array(
'label' => __( 'Scroll after navigation', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'top' => __( 'Scroll to the top of the page', 'ajax-loading' ),
'content' => __( 'Scroll to the top of the content area', 'ajax-loading' ),
'none' => __( 'Do not scroll', 'ajax-loading' ),
),
'desc' => __( 'Links to a #section always scroll to that section. Back and forward restore the previous position (see Performance).', 'ajax-loading' ),
),
'focus_target' => array(
'label' => __( 'Move keyboard focus to', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'heading' => __( 'The main heading (recommended)', 'ajax-loading' ),
'container' => __( 'The content area', 'ajax-loading' ),
'none' => __( 'Leave focus where it is', 'ajax-loading' ),
),
'desc' => __( 'Keyboard and screen reader users continue from here on the new page.', 'ajax-loading' ),
),
'announce_navigation' => array(
'label' => __( 'Announce page changes', 'ajax-loading' ),
'desc' => __( 'Tell screen reader users that a new page has loaded, with its title.', 'ajax-loading' ),
),
// Performance.
'prefetch_enabled' => array(
'label' => __( 'Prefetch links', 'ajax-loading' ),
'desc' => __( 'Uses a little extra bandwidth to make navigation feel instant.', 'ajax-loading' ),
),
'prefetch_triggers' => array(
'label' => __( 'Prefetch when a link is', 'ajax-loading' ),
'choices' => array(
'hover' => __( 'Hovered with the mouse', 'ajax-loading' ),
'focus' => __( 'Focused with the keyboard', 'ajax-loading' ),
'touch' => __( 'Touched (before the tap completes)', 'ajax-loading' ),
'viewport' => __( 'Visible on screen', 'ajax-loading' ),
),
'desc' => __( 'When a link starts loading in the background. Choose one or more.', 'ajax-loading' ),
'show_if' => $prefetch_on,
),
'prefetch_hover_delay' => array(
'label' => __( 'Hover delay', 'ajax-loading' ),
'desc' => __( 'How long the pointer must rest on a link before it is prefetched.', 'ajax-loading' ),
'unit' => $ms,
'show_if' => array_merge(
$prefetch_on,
array(
array(
'fields' => array( 'prefetch_triggers' ),
'in' => array( 'hover' ),
),
)
),
),
'prefetch_max_concurrent' => array(
'label' => __( 'Parallel prefetches', 'ajax-loading' ),
'desc' => __( 'Maximum number of pages prefetched at the same time.', 'ajax-loading' ),
'show_if' => $prefetch_on,
),
'prefetch_viewport_limit' => array(
'label' => __( 'Visible links per page', 'ajax-loading' ),
'desc' => __( 'Maximum number of on-screen links prefetched per page.', 'ajax-loading' ),
'show_if' => array_merge(
$prefetch_on,
array(
array(
'fields' => array( 'prefetch_triggers' ),
'in' => array( 'viewport' ),
),
)
),
),
'prefetch_exclude' => array(
'label' => __( 'Never prefetch', 'ajax-loading' ),
'desc' => __( 'URL patterns, one per line, using the same syntax as the Exclusions tab. Use it for pages that are expensive to generate.', 'ajax-loading' ),
'placeholder' => '/search/*',
'rows' => 3,
'show_if' => $prefetch_on,
),
'network_aware' => array(
'label' => __( 'Respect data saver', 'ajax-loading' ),
'desc' => __( 'Skip prefetching when the visitor has data saver on or a very slow connection.', 'ajax-loading' ),
'show_if' => $prefetch_on,
),
'cache_enabled' => array(
'label' => __( 'Cache visited pages', 'ajax-loading' ),
'desc' => __( 'Returning to a recently visited page is instant. Logged-in visitors and personalised pages are never cached.', 'ajax-loading' ),
),
'cache_max_pages' => array(
'label' => __( 'Pages to keep', 'ajax-loading' ),
'desc' => __( 'Maximum number of pages kept. The oldest page is dropped first when the limit is reached.', 'ajax-loading' ),
'show_if' => array(
array(
'fields' => array( 'cache_enabled' ),
'in' => array( '1' ),
),
),
),
'cache_ttl' => array(
'label' => __( 'Keep pages for', 'ajax-loading' ),
'desc' => __( 'How long a cached page may be reused.', 'ajax-loading' ),
'unit' => $seconds,
'show_if' => array(
array(
'fields' => array( 'cache_enabled' ),
'in' => array( '1' ),
),
),
),
'cache_max_kb' => array(
'label' => __( 'Memory budget', 'ajax-loading' ),
'desc' => __( 'Upper limit for the HTML kept in memory.', 'ajax-loading' ),
'unit' => _x( 'KB', 'kilobytes unit', 'ajax-loading' ),
'show_if' => array(
array(
'fields' => array( 'cache_enabled' ),
'in' => array( '1' ),
),
),
),
'request_timeout' => array(
'label' => __( 'Request timeout', 'ajax-loading' ),
'desc' => __( 'If a page takes longer than this, the browser loads it normally instead.', 'ajax-loading' ),
'unit' => $ms,
),
'disable_core_speculation' => array(
'label' => __( 'Turn off WordPress speculative loading', 'ajax-loading' ),
'desc' => __( 'WordPress 6.8 and later prefetch links on their own. Turning this off while AJAX navigation is active avoids downloading pages twice.', 'ajax-loading' ),
),
'preserve_scroll' => array(
'label' => __( 'Restore scroll position', 'ajax-loading' ),
'desc' => __( 'Back and forward return to where the visitor was on that page.', 'ajax-loading' ),
),
'preserve_form_state' => array(
'label' => __( 'Restore search and filter inputs', 'ajax-loading' ),
'desc' => __( 'Back and forward restore what the visitor typed into search and filter forms.', 'ajax-loading' ),
),
'sync_menu_state' => array(
'label' => __( 'Highlight the current menu item', 'ajax-loading' ),
'desc' => __( 'Keep your menus\' "current page" highlighting and its screen reader equivalent in sync.', 'ajax-loading' ),
),
// SEO and integrations.
'sync_metadata' => array(
'label' => __( 'Update meta tags', 'ajax-loading' ),
'desc' => __( 'Description, robots, Open Graph, Twitter cards and similar tags.', 'ajax-loading' ),
),
'sync_canonical' => array(
'label' => __( 'Update canonical and alternate links', 'ajax-loading' ),
'desc' => __( 'Canonical URL, previous/next, shortlink and language alternates (hreflang).', 'ajax-loading' ),
),
'sync_structured_data' => array(
'label' => __( 'Update structured data', 'ajax-loading' ),
'desc' => __( 'Swap the JSON-LD (schema.org) blocks in the page head. They are never executed.', 'ajax-loading' ),
),
'analytics_mode' => array(
'label' => __( 'Page view tracking', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'none' => __( 'Automatic: let Google Analytics or Tag Manager detect navigations', 'ajax-loading' ),
'gtag' => __( 'Send GA4 page_view events (gtag.js)', 'ajax-loading' ),
'datalayer' => __( 'Push a dataLayer event for Google Tag Manager', 'ajax-loading' ),
),
'choice_desc' => array(
'none' => __( 'Recommended. GA4 Enhanced Measurement ("Page changes based on browser history events") or a GTM History Change trigger counts each navigation.', 'ajax-loading' ),
'gtag' => __( 'Requires gtag.js on your site. Turn off GA4\'s history-based page views, or every navigation is counted twice.', 'ajax-loading' ),
'datalayer' => __( 'Create a Custom Event trigger in Tag Manager for the event name below.', 'ajax-loading' ),
),
'notice' => array(
'status' => 'warning',
'in' => array( 'gtag' ),
'text' => sprintf(
/* translators: 1: opening strong tag, 2: closing strong tag, 3: GA4 option name. */
__( '%1$sAvoid double-counted page views.%2$s In Google Analytics go to Admin → Data streams → your web stream → Enhanced measurement → Page views → Show advanced settings, and turn off %3$s. Otherwise GA4 counts every AJAX navigation a second time.', 'ajax-loading' ),
'',
'',
'' . esc_html__( 'Page changes based on browser history events', 'ajax-loading' ) . ''
),
),
),
'analytics_timing' => array(
'label' => __( 'Send navigation timing', 'ajax-loading' ),
'desc' => __( 'Also send how long each AJAX navigation took, in milliseconds.', 'ajax-loading' ),
'show_if' => array(
array(
'fields' => array( 'analytics_mode' ),
'in' => array( 'gtag', 'datalayer' ),
),
),
),
'gtm_event_name' => array(
'label' => __( 'dataLayer event name', 'ajax-loading' ),
'desc' => __( 'Letters, numbers, dots, dashes and underscores; up to 64 characters.', 'ajax-loading' ),
'placeholder' => 'ajax_page_view',
'maxlength' => 64,
'pattern' => '[A-Za-z0-9_.\-]{1,64}',
'show_if' => array(
array(
'fields' => array( 'analytics_mode' ),
'in' => array( 'datalayer' ),
),
),
),
'analytics_require_consent' => array(
'label' => __( 'Require statistics consent', 'ajax-loading' ),
'desc' => sprintf(
/* translators: %s: consent category name. */
__( 'Only track navigations when the WP Consent API reports consent for %s. Has no effect if no consent plugin is installed.', 'ajax-loading' ),
'statistics'
),
),
'analytics_callback' => array(
'label' => __( 'Custom tracking function', 'ajax-loading' ),
'desc' => __( 'Name of a global JavaScript function (dotted paths allowed) called after every navigation with the navigation details. Leave empty for none.', 'ajax-loading' ),
'placeholder' => 'myTheme.trackPage',
'maxlength' => 100,
'pattern' => '[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*',
),
'integrations' => array(
'label' => __( 'Integrations', 'ajax-loading' ),
'desc' => __( 'How AJAX navigation handles each plugin or feature. "Detected" means it is active on this site.', 'ajax-loading' ),
),
// Exclusions.
'exclude_urls' => array(
'label' => __( 'Excluded URLs', 'ajax-loading' ),
'desc' => __( 'One pattern per line. See the pattern syntax below.', 'ajax-loading' ),
'placeholder' => "/shop/*\n/landing-page/",
'rows' => 5,
),
'exclude_selectors' => array(
'label' => __( 'Excluded links (CSS selectors)', 'ajax-loading' ),
'desc' => __( 'Links inside or matching these selectors load normally. One selector per line.', 'ajax-loading' ),
'placeholder' => ".main-menu .external\n#footer-links a",
'rows' => 4,
),
'exclude_post_types' => array(
'label' => __( 'Excluded post types', 'ajax-loading' ),
'desc' => __( 'Single entries and archives of these types always load normally.', 'ajax-loading' ),
),
'exclude_woocommerce' => array(
'label' => __( 'Protect WooCommerce pages', 'ajax-loading' ),
'desc' => __( 'Cart, checkout, My Account, order pages and cart actions always load normally.', 'ajax-loading' ),
),
'exclude_membership' => array(
'label' => __( 'Protect login and membership pages', 'ajax-loading' ),
'desc' => __( 'Login, registration, checkout and account pages of popular membership plugins load normally.', 'ajax-loading' ),
),
'interactive_blocks' => array(
'label' => __( 'Interactive blocks', 'ajax-loading' ),
'control' => 'radio',
'choices' => array(
'reload' => __( 'Load these pages normally (recommended)', 'ajax-loading' ),
'replace' => __( 'Replace them with AJAX', 'ajax-loading' ),
),
'desc' => __( 'Content built with the WordPress Interactivity API (for example some Query Loop, Search and Navigation blocks) often stops working after an AJAX swap.', 'ajax-loading' ),
),
// Tools.
'safe_mode' => array(
'label' => __( 'Safe mode', 'ajax-loading' ),
'desc' => __( 'Conservative behaviour for troubleshooting: no prefetching or caching, header and footer kept, page builder pages load normally, no synthetic events.', 'ajax-loading' ),
),
'auto_disable' => array(
'label' => __( 'Automatic fallback', 'ajax-loading' ),
'desc' => __( 'If navigation fails repeatedly for a visitor, switch that visitor to normal page loads for a while.', 'ajax-loading' ),
),
'auto_disable_threshold' => array(
'label' => __( 'Failures before fallback', 'ajax-loading' ),
'desc' => __( 'Number of failed navigations within 5 minutes.', 'ajax-loading' ),
'show_if' => array(
array(
'fields' => array( 'auto_disable' ),
'in' => array( '1' ),
),
),
),
'auto_disable_minutes' => array(
'label' => __( 'Fallback duration', 'ajax-loading' ),
'desc' => __( 'How long the visitor gets normal page loads after repeated failures.', 'ajax-loading' ),
'unit' => $minutes,
'show_if' => array(
array(
'fields' => array( 'auto_disable' ),
'in' => array( '1' ),
),
),
),
'debug_mode' => array(
'label' => __( 'Debug logging', 'ajax-loading' ),
'desc' => __( 'Write navigation details to the browser console. Turn off on production sites.', 'ajax-loading' ),
),
'error_logging' => array(
'label' => __( 'Record navigation errors', 'ajax-loading' ),
'desc' => __( 'Store the last 50 failed navigations (error code and page path only, no personal data) and show them under Recent errors.', 'ajax-loading' ),
),
);
return self::$meta;
}
/**
* Metadata for one key (with a readable fallback label).
*
* @param string $key Setting key.
* @return array
*/
public static function field_meta( $key ) {
$meta = self::meta();
$item = isset( $meta[ $key ] ) ? $meta[ $key ] : array();
if ( empty( $item['label'] ) ) {
$item['label'] = ucfirst( str_replace( '_', ' ', (string) $key ) );
}
return $item;
}
/**
* Dashboard tab a schema entry belongs to ('' = not shown).
*
* @param string $key Setting key.
* @param mixed $def Schema entry.
* @return string
*/
public static function ui_tab_of( $key, $def ) {
if ( in_array( $key, self::HIDDEN_KEYS, true ) || ! is_array( $def ) || empty( $def['tab'] ) || ! is_string( $def['tab'] ) ) {
return '';
}
$tab = strtolower( $def['tab'] );
if ( 'advanced' === $tab || 0 === strpos( $tab, 'general' ) ) {
return 'general';
}
return in_array( $tab, self::SETTINGS_TABS, true ) ? $tab : '';
}
/**
* Schema keys rendered on a tab, in schema order.
*
* @param array $schema Settings schema.
* @param string $tab Tab slug.
* @return string[]
*/
public static function keys_for_tab( array $schema, $tab ) {
$keys = array();
foreach ( $schema as $key => $def ) {
if ( self::ui_tab_of( $key, $def ) === $tab ) {
$keys[] = (string) $key;
}
}
return $keys;
}
/**
* Map of key => type for every visible field (consumed by the admin script).
*
* @param array $schema Settings schema.
* @return array
*/
public static function types( array $schema ) {
$types = array();
foreach ( $schema as $key => $def ) {
if ( '' !== self::ui_tab_of( $key, $def ) && ! empty( $def['type'] ) ) {
$types[ $key ] = (string) $def['type'];
}
}
return $types;
}
/**
* Normalised list of allowed values of an enum/multi schema entry.
*
* Accepts a plain list of values or a value => label map.
*
* @param array $def Schema entry.
* @return string[]
*/
public static function choice_values( array $def ) {
if ( empty( $def['choices'] ) || ! is_array( $def['choices'] ) ) {
return array();
}
$choices = $def['choices'];
$is_list = array_keys( $choices ) === range( 0, count( $choices ) - 1 );
$raw = $is_list ? array_values( $choices ) : array_keys( $choices );
$values = array();
foreach ( $raw as $value ) {
if ( is_scalar( $value ) && '' !== (string) $value ) {
$values[] = (string) $value;
}
}
return array_values( array_unique( $values ) );
}
/**
* Human label of a choice value.
*
* @param string $key Setting key.
* @param string $value Choice value.
* @param array $def Optional schema entry (value => label maps are used as a fallback).
* @return string
*/
public static function choice_label( $key, $value, array $def = array() ) {
$meta = self::field_meta( $key );
$value = (string) $value;
if ( isset( $meta['choices'][ $value ] ) ) {
return $meta['choices'][ $value ];
}
if ( isset( $def['choices'][ $value ] ) && is_string( $def['choices'][ $value ] ) ) {
return $def['choices'][ $value ];
}
return ucfirst( str_replace( array( '-', '_' ), ' ', $value ) );
}
/**
* Input name for a key.
*
* @param string $key Setting key.
* @param string $suffix Optional extra bracket part (e.g. '[]' or '[woocommerce]').
* @return string
*/
public static function name( $key, $suffix = '' ) {
return self::OPTION . '[' . $key . ']' . $suffix;
}
/**
* Element id for a key.
*
* @param string $key Setting key.
* @param string $suffix Optional suffix.
* @return string
*/
public static function id( $key, $suffix = '' ) {
$id = 'ajxl-field-' . str_replace( '_', '-', sanitize_key( $key ) );
return '' === $suffix ? $id : $id . '-' . sanitize_key( $suffix );
}
/**
* Loose boolean conversion for stored values.
*
* @param mixed $value Value.
* @return bool
*/
public static function to_bool( $value ) {
if ( is_bool( $value ) ) {
return $value;
}
if ( is_string( $value ) ) {
return in_array( strtolower( $value ), array( '1', 'on', 'yes', 'true' ), true );
}
return ! empty( $value );
}
/**
* Render all sections of a settings tab.
*
* Fields of the tab that are not listed in a known section (for example
* fields added through the 'ajax_loading_settings_schema' filter) are
* rendered in a trailing "More settings" section so none is ever lost.
*
* @param string $tab Tab slug.
* @param array $schema Settings schema.
* @param array $values Stored settings.
* @return void
*/
public static function render_tab( $tab, array $schema, array $values ) {
$tab_keys = self::keys_for_tab( $schema, $tab );
$sections = self::sections();
$sections = isset( $sections[ $tab ] ) ? $sections[ $tab ] : array();
$placed = array();
foreach ( $sections as $section ) {
$keys = array_values( array_intersect( $section['keys'], $tab_keys ) );
if ( empty( $keys ) ) {
continue;
}
$placed = array_merge( $placed, $keys );
self::render_section( $section, $keys, $schema, $values );
}
$rest = array_values( array_diff( $tab_keys, $placed ) );
if ( ! empty( $rest ) ) {
self::render_section(
array(
'id' => 'more',
'title' => __( 'More settings', 'ajax-loading' ),
'intro' => '',
),
$rest,
$schema,
$values
);
}
}
/**
* Render one section card.
*
* @param array $section Section definition.
* @param array $keys Keys to render.
* @param array $schema Settings schema.
* @param array $values Stored settings.
* @return void
*/
public static function render_section( array $section, array $keys, array $schema, array $values ) {
$collapsible = ! empty( $section['collapsible'] );
$id = 'ajxl-section-' . sanitize_key( $section['id'] );
$table_keys = array();
$block_keys = array();
foreach ( $keys as $key ) {
$type = isset( $schema[ $key ]['type'] ) ? $schema[ $key ]['type'] : 'text';
if ( 'map' === $type ) {
$block_keys[] = $key;
} else {
$table_keys[] = $key;
}
}
if ( $collapsible ) {
printf( '' . esc_html( $section['intro'] ) . '
'; } if ( ! empty( $table_keys ) ) { echo '%s
', esc_attr( $id ), wp_kses( $meta['desc'], self::allowed_html() ) ); } echo '%4$s
%2$s
', esc_attr( $id ), wp_kses( $meta['desc'], self::allowed_html() ) ); $described = sprintf( ' aria-describedby="%s-desc"', esc_attr( $id ) ); } printf( '| ' . esc_html__( 'Integration', 'ajax-loading' ) . ' | '; echo '' . esc_html__( 'On this site', 'ajax-loading' ) . ' | '; echo '' . esc_html__( 'Behaviour', 'ajax-loading' ) . ' | '; echo '
|---|---|---|
| ';
if ( ! empty( $info['description'] ) ) {
echo ' ' . esc_html( (string) $info['description'] ) . ' '; } echo ' | '; if ( array_key_exists( 'active', $info ) ) { echo ! empty( $info['active'] ) ? WPAJXL_Admin::pill( 'ok', __( 'Detected', 'ajax-loading' ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pill() escapes. : WPAJXL_Admin::pill( 'neutral', __( 'Not detected', 'ajax-loading' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pill() escapes. } else { echo '—'; } echo ' | '; printf( ' |
| ' . esc_html__( 'No integrations are available.', 'ajax-loading' ) . ' | ||