/home/bonphmya/mercandestockages.store/wp-content/plugins/jetpack/_inc/site-switcher.js
/**
* Site Switcher for Command Palette
* Adds a dynamic "Switch to Site" command that searches across all user's WordPress.com sites
*
* Requires WordPress 6.9+ for admin-wide command palette support
*
* @package
*/
import apiFetch from '@wordpress/api-fetch';
import { useCommandLoader } from '@wordpress/commands';
import { useMemo, useState, useEffect } from '@wordpress/element';
import { sprintf, __ } from '@wordpress/i18n';
import { siteLogo } from '@wordpress/icons';
const userId = window.jetpackSiteSwitcherConfig?.userId || 0;
const CACHE_KEY = `jetpack_site_switcher_sites_${ userId }`;
const CACHE_DURATION = 3600000; // 1 hour in milliseconds
/**
* Get cached sites from localStorage
*/
function getCachedSites() {
try {
const cached = localStorage.getItem( CACHE_KEY );
if ( ! cached ) {
return null;
}
const { sites, timestamp } = JSON.parse( cached );
// Check if cache is still valid
if ( Date.now() - timestamp < CACHE_DURATION ) {
return sites;
}
// Cache expired, remove it
localStorage.removeItem( CACHE_KEY );
return null;
} catch {
// If localStorage is not available or JSON parsing fails, return null
return null;
}
}
/**
* Save sites to localStorage cache
*/
function setCachedSites( sites ) {
try {
localStorage.setItem(
CACHE_KEY,
JSON.stringify( {
sites,
timestamp: Date.now(),
} )
);
} catch {
// Silently fail if localStorage is not available (e.g., private browsing)
}
}
/**
* Fetch compact sites list from WordPress.com API
*/
async function fetchSitesFromWordPressCom() {
// Check localStorage cache first
const cachedSites = getCachedSites();
if ( cachedSites ) {
return cachedSites;
}
const apiPath = window.jetpackSiteSwitcherConfig?.apiPath;
try {
const data = await apiFetch( {
path: apiPath,
method: 'GET',
global: true,
} );
const sites = data.sites || [];
setCachedSites( sites );
return sites;
} catch {
return [];
}
}
/**
* Safely extract hostname from a URL string
*
* @param {string} urlString - The URL to parse
* @return {string} The hostname, or empty string if invalid
*/
function getHostnameFromURL( urlString ) {
if ( ! urlString ) {
return '';
}
try {
return new URL( urlString ).hostname;
} catch {
return '';
}
}
/**
* Remove trailing slash from a URL string
*
* @param {string} url - The URL to process
* @return {string} URL without trailing slash
*/
function untrailingslashit( url ) {
return url ? url.replace( /\/+$/, '' ) : url;
}
/**
* Escape special regex characters in a string
*
* @param {string} str - String to escape
* @return {string} Escaped string safe for use in RegExp
*/
function escapeRegex( str ) {
return str.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
}
/**
* Custom hook to load site-switching commands based on search term
*
* @param {Object} props - Hook properties
* @param {string} props.search - Search term to filter sites
* @return {Object} Object containing commands array and loading state
*/
function useSiteSwitcherCommandLoader( { search } ) {
const [ sites, setSites ] = useState( [] );
const [ isLoading, setIsLoading ] = useState( true );
// Fetch sites on mount
useEffect( () => {
fetchSitesFromWordPressCom()
.then( fetchedSites => {
setSites( fetchedSites );
setIsLoading( false );
} )
.catch( () => {
setIsLoading( false );
} );
}, [] );
// Generate and filter commands based on search term
const commands = useMemo( () => {
if ( ! sites || sites.length === 0 ) {
return [];
}
const searchLower = search ? search.toLowerCase() : '';
// Strip generic keywords from search to allow queries like "site dean" to find sites with "dean"
const genericKeywords = [
__( 'site', 'jetpack' ).toLowerCase(),
__( 'switch', 'jetpack' ).toLowerCase(),
__( 'switch site', 'jetpack' ).toLowerCase(),
];
let cleanedSearch = searchLower;
genericKeywords.forEach( keyword => {
cleanedSearch = cleanedSearch.replace(
new RegExp( `\\b${ escapeRegex( keyword ) }\\b`, 'g' ),
' '
);
} );
cleanedSearch = cleanedSearch.trim().replace( /\s+/g, ' ' );
// Check if the search is a prefix of any generic keyword (e.g., "swit" matches "switch")
// If so, treat it as a generic search and show all sites
const isGenericKeywordPrefix =
cleanedSearch && genericKeywords.some( keyword => keyword.startsWith( cleanedSearch ) );
// If search is empty after stripping generic keywords, or is a prefix of a generic keyword, show all sites
const filteredSites =
! cleanedSearch || isGenericKeywordPrefix
? sites
: sites.filter( site => {
const domain = getHostnameFromURL( site.URL );
return (
( site.name && site.name.toLowerCase().includes( cleanedSearch ) ) ||
domain.toLowerCase().includes( cleanedSearch )
);
} );
// Filter out sites with invalid URLs (can't navigate to them anyway)
const validSites = filteredSites.filter( site => {
return site.URL && getHostnameFromURL( site.URL ) !== '';
} );
// Exclude the current site from the list
const currentURL = untrailingslashit( window.location.href.toLowerCase() );
const otherSites = validSites.filter( site => {
// Normalize site URL for comparison
const siteURL = untrailingslashit( site.URL.toLowerCase() );
// Check if current URL starts with site URL (handles multisite subdirectory installs)
// e.g., current: example.com/site1/wp-admin matches site: example.com/site1
return ! currentURL.startsWith( siteURL );
} );
return otherSites.map( site => {
// Extract domain from URL for display - don't want to display the protocol.
const domain = getHostnameFromURL( site.URL );
const iconElement = site.icon?.img ? <img src={ site.icon.img } alt="" /> : siteLogo;
// Use site name if available, otherwise just show domain
const label = site.name
? sprintf(
/* translators: %1$s: site name, %2$s: site domain */
__( 'Switch to %1$s (%2$s)', 'jetpack' ),
site.name,
domain
)
: sprintf(
/* translators: %s: site domain */
__( 'Switch to %s', 'jetpack' ),
domain
);
return {
name: `jetpack/switch-to-site-${ domain }`,
label,
icon: iconElement,
callback: ( { close } ) => {
try {
window.location.href = new URL( '/wp-admin', site.URL ).href;
} catch {
// If URL is malformed, don't navigate
}
close();
},
keywords: [
site.name,
domain,
__( 'site', 'jetpack' ),
__( 'switch site', 'jetpack' ),
].filter( Boolean ),
};
} );
}, [ sites, search ] );
return {
commands,
isLoading,
};
}
/**
* Component that registers the site switcher command loader
*/
function JetpackSiteSwitcher() {
useCommandLoader( {
name: 'jetpack/site-switcher',
hook: useSiteSwitcherCommandLoader,
} );
return null;
}
// Render the site switcher into wp-admin
// This works with WordPress 6.9+ admin-wide command palette
if ( typeof window !== 'undefined' && window.wp && window.wp.element && window.wp.commands ) {
const { createRoot, createElement } = window.wp.element;
// Create a container for our site switcher
const container = document.createElement( 'div' );
container.id = 'jetpack-site-switcher';
container.style.display = 'none'; // Hidden, as we only need the hooks to run
// Wait for DOM to be ready
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', () => {
document.body.appendChild( container );
createRoot( container ).render( createElement( JetpackSiteSwitcher ) );
} );
} else {
document.body.appendChild( container );
createRoot( container ).render( createElement( JetpackSiteSwitcher ) );
}
}