Current path: home/fresvfqn/emergencywaterdamagemanhattan.com/wp-content/plugins/surerank/src/functions/
⬆️ Go up: src
import { __, _n, sprintf } from '@wordpress/i18n';
import {
format as format_date,
startOfDay,
startOfToday,
startOfYesterday,
} from 'date-fns';
import clsx from 'clsx';
import { createRoot } from 'react-dom';
import { twMerge } from 'tailwind-merge';
export const cleanContent = ( postContent ) => {
// Get first paragraph. tag will be <p>.
const content = postContent.match( /<p>(.*?)<\/p>/g );
// If paragraph is found. then get the content.
if ( content?.length ) {
return content[ 0 ].replace( /(<([^>]+)>)/gi, '' );
}
// Remove all the tags from the content and return it.
const removedAllTags = postContent.replace( /(<([^>]+)>)/gi, '' );
// Remove extra spaces.
const removedExtraSpaces = removedAllTags.replace( /\s+/g, ' ' );
return removedExtraSpaces;
};
/**
* TruncateText function truncates the provided text to the specified length.
* If the length of the text exceeds the specified length, it is truncated and a suffix is appended.
*
* @param {string} text - The text to truncate.
* @param {number} maxLength - The maximum length of the truncated text.
* @param {string} [suffix="..."] - The suffix to append if the text is truncated. Default is three dots.
* @return {string} The truncated text.
*/
export const truncateText = ( text, maxLength, suffix = '...' ) => {
// If maxLength is not provided or is less than 0, return the text as it is.
if ( ! text?.length || ! maxLength || maxLength < 0 ) {
return text;
}
return text.length <= maxLength
? text
: text.slice( 0, maxLength ) + suffix;
};
/**
* Mounts a React component onto a specified DOM element.
* If the target element doesn't exist, an error will be logged to the console.
*
* @param {string} selector - The CSS selector for the target DOM element.
* @param {Function} Component - The React component to render.
* @param {number} [timeout=100] - The delay in milliseconds before rendering the component.
* @return {void}
*/
export const mountComponent = ( selector, Component, timeout = 100 ) => {
// Validate selector parameters
if ( typeof selector !== 'string' || ! selector.trim() ) {
// eslint-disable-next-line no-console
console.error( 'Invalid selector provided.' );
return;
}
// Validate Component parameters this should be react component
if ( ! isReactComponent( Component ) ) {
// eslint-disable-next-line no-console
console.error( 'Invalid React component provided.' );
return;
}
// Check if the target element exists in the DOM
const targetElement = document.querySelector( selector );
// Log an error if the target element is not found
if ( ! targetElement ) {
// eslint-disable-next-line no-console
console.error(
`Target element with selector '${ selector }' not found.`
);
return;
}
// Render the component after a timeout
setTimeout( () => {
const root = createRoot( targetElement );
root.render( Component );
}, timeout );
};
// Example of checking if a variable is a React component
export const isReactComponent = ( variable ) => {
// Check by verifying the presence of the `$$typeof` property Symbol(react.element) in the variable.
return variable && variable?.$$typeof === Symbol.for( 'react.element' );
};
/**
* Get settings page name based on the main URL.
*
* @return {string} - The settings page name.
*/
export const getSettingsPageName = () => {
const settingsPages = {
surerank_general: 'general_settings',
surerank_social: 'social_settings',
surerank_advanced: 'advanced_settings',
};
const urlParams = new URLSearchParams( window.location.search );
const page = urlParams.get( 'page' );
return settingsPages[ page ] || 'general_settings';
};
/**
* Utility function to merge Tailwind CSS and conditional class names.
* @param {...any} args
*/
export const cn = ( ...args ) => twMerge( clsx( ...args ) );
/**
* Parse lexical editor value to get the content as a string.
*
* @param {Object} valueObj - The lexical editor value.
* @param {string} optionValueKey to get the value from the mention node type.
* @return {string} - The content as a string.
*/
export const editorValueToString = ( valueObj, optionValueKey = 'value' ) => {
const value = valueObj?.root?.children[ 0 ]?.children;
if ( ! value || ! value?.length ) {
return '';
}
let stringContent = '';
value.forEach( ( child ) => {
switch ( child.type ) {
case 'text':
stringContent += child.text;
break;
case 'mention':
stringContent += child.data[ optionValueKey ];
break;
case 'linebreak':
stringContent += '\n';
break;
default:
break;
}
} );
return stringContent;
};
/**
* Parse the content from string to the lexical editor value (json object).
*
* @param {string} stringContent - The content as a string.
* @param {object[]} options - The options array to replace the mention value.
* @param {string} optionValueKey - The key to get the value from the mention object.
* @param {Object} mentionObjectStructure - The mention object to replace the mention object structure.
*
* @return {JSON} - The lexical editor value.
*/
export const stringValueToFormatJSON = (
stringContent,
options = [],
optionValueKey = 'value',
mentionObjectStructure = {
type: 'mention',
version: 1,
data: {},
size: 'md',
by: 'label',
}
) => {
const initialValue = {
root: {
children: [
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
textFormat: 0,
textStyle: '',
},
],
direction: null,
format: '',
indent: 0,
type: 'root',
version: 1,
},
};
const value = { ...initialValue };
const content = ( typeof stringContent === 'string' ? stringContent : '' )
.trim()
.split( /(\s+|%[\w\-_.]+%)/ ) // Split on spaces or %mention%
.filter( Boolean );
content.forEach( ( item ) => {
if ( item === '\n' ) {
value.root.children[ 0 ].children.push( {
type: 'linebreak',
version: 1,
} );
} else if ( item?.startsWith( '%' ) && item?.endsWith( '%' ) ) {
const option = options?.find(
( mentionItem ) => mentionItem[ optionValueKey ] === item.trim()
);
if ( option ) {
value.root.children[ 0 ].children.push( {
...mentionObjectStructure,
data: { ...option },
} );
}
} else {
value.root.children[ 0 ].children.push( {
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: item,
type: 'text',
version: 1,
} );
}
} );
return JSON.stringify( value );
};
/**
* Converts a given URL into a formatted string with a maximum character limit.
*
* @param {string} url - The URL to be converted.
* @param {number} maxChar - The maximum number of characters allowed in the converted URL.
* @return {string} The converted URL string.
*
* @example
* // Convert the URL and limit the converted string to 20 characters
* const converted = urlToBreadcrumbFormat('https://example.com/some/long/path', 20);
* console.log(converted);
* // Output: "https://example.com › some › long › ..."
*/
export function urlToBreadcrumbFormat( url, maxChar = 65 ) {
const urlParts = url.split( '/' );
const domain = urlParts.slice( 0, 3 ).join( '/' );
const path = urlParts.slice( 3 ).filter( Boolean ).join( ' › ' );
let convertedUrl = `${ domain } › ${ path }`;
if ( convertedUrl.length > maxChar ) {
convertedUrl = convertedUrl.substring( 0, maxChar - 3 ) + '...';
}
return convertedUrl;
}
/**
* Checks if the current page is the specified page.
* The parameter can be a string or an array of strings.
*
* @param {string|string[]} pages - The page(s) to check against.
* @return {boolean} - True if the current page is term.php and matches the provided page(s), otherwise false.
*/
export const isCurrentPage = ( pages ) => {
const currentPage = window.location.pathname;
if ( Array.isArray( pages ) ) {
return pages.some( ( page ) => currentPage.includes( page ) );
}
return currentPage.includes( pages );
};
/**
* Deep clones an object.
*
* @param {Object} obj - The object to clone.
* @return {Object} The cloned object.
*/
export const deepClone = ( obj ) => {
if ( obj === null || typeof obj !== 'object' ) {
return obj;
}
if ( Array.isArray( obj ) ) {
return obj.map( deepClone );
}
return Object.fromEntries(
Object.entries( obj ).map( ( [ key, value ] ) => [
key,
deepClone( value ),
] )
);
};
/**
* Scrolls to an element with highlighting.
*
* @param {string} elementId - The ID of the element to scroll to.
* @param {Object} options - The options for the scroll to element.
* @param {number} options.delay - The delay in milliseconds before scrolling to the element.
* @param {number} options.retryDelay - The delay in milliseconds between retries.
* @param {number} options.maxRetries - The maximum number of retries.
*/
export const scrollToElement = ( elementId, options = {} ) => {
if ( ! elementId ) {
return;
}
const { delay = 1000, retryDelay = 200, maxRetries = 5 } = options;
// Function to attempt scroll with retry mechanism
const attemptScrollToElement = ( retryCount = 0 ) => {
// Find the target element by ID
const targetElement = document.getElementById( elementId );
if ( targetElement ) {
// Element found, scroll it into view
setTimeout( () => {
targetElement.scrollIntoView( {
behavior: 'smooth',
block: 'center',
inline: 'nearest',
} );
}, delay );
} else if ( retryCount < maxRetries ) {
// Element not found yet, retry after a delay
setTimeout( () => {
attemptScrollToElement( retryCount + 1 );
}, retryDelay );
}
};
// Use requestAnimationFrame for the initial attempt
window.requestAnimationFrame( () => {
attemptScrollToElement();
} );
};
/**
* Checks if the current query parameter matches the specified value.
*
* @param {string} queryParam - The query parameter to check.
* @param {string} value - The value to compare against.
* @return {boolean} - True if the query parameter matches the value, otherwise false.
*/
export const isEqualQueryParamValue = ( queryParam, value ) => {
try {
const urlObj = new URL( window.location.href );
return urlObj.searchParams.get( queryParam ) === value;
} catch ( error ) {
return false;
}
};
/**
* Formats a given date string based on the provided options.
* If no options are provided, it defaults to 'yyyy-MM-dd' format.
*
* @param {string|Date} date - The date string or Date object to format.
* @param {string} [dateFormat='yyyy-MM-dd'] - The date format string for `date-fns`.
* @return {string} - The formatted date string or a fallback if the input is invalid.
*/
export const format = ( date, dateFormat = 'yyyy-MM-dd' ) => {
try {
if ( ! date || isNaN( new Date( date ).getTime() ) ) {
throw new Error( __( 'Invalid Date', 'surerank' ) );
}
return format_date( new Date( date ), dateFormat );
} catch ( error ) {
return __( 'No Date', 'surerank' );
}
};
/**
* Formats a given date string based on the provided options.
*
* @param {string} dateString - The date string to format.
* @param {Object} options - Formatting options to customize the output.
* @param {boolean} [options.day] - Whether to include the day in the output.
* @param {boolean} [options.month] - Whether to include the month in the output.
* @param {boolean} [options.year] - Whether to include the year in the output.
* @param {boolean} [options.hour] - Whether to include the hour in the output.
* @param {boolean} [options.minute] - Whether to include the minute in the output.
* @param {boolean} [options.hour12] - Whether to use a 12-hour clock format.
* @return {string} - The formatted date string or a fallback if the input is invalid.
*/
export const formatDate = ( dateString, options = {} ) => {
if ( ! dateString || isNaN( new Date( dateString ).getTime() ) ) {
return __( 'No Date', 'surerank' );
}
const optionMap = {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true, // Note: hour12 is a boolean directly
};
const formattingOptions = Object.keys( optionMap ).reduce( ( acc, key ) => {
if ( options[ key ] === true ) {
acc[ key ] = optionMap[ key ];
} else if ( options[ key ] === false ) {
} else if ( options[ key ] !== undefined ) {
acc[ key ] = options[ key ];
}
return acc;
}, {} );
return new Intl.DateTimeFormat( 'en-US', formattingOptions ).format(
new Date( dateString )
);
};
/**
* Get the last N days date range
*
* @param {number} days - The number of days to get the date range for.
* @param {boolean} [includeToday=true] - Whether to include today in the date range.
* @return {Object} - The date range.
*/
export const getLastNDays = ( days, includeToday = true ) => {
if ( isNaN( days ) ) {
return {
from: '',
to: '',
};
}
const endDate = includeToday ? startOfToday() : startOfYesterday();
let startDate = new Date( endDate );
// When calculating for N days, we need to go back (N-1) days from the end date
// to include the end date itself in the count
startDate.setDate( endDate.getDate() - days );
startDate = startOfDay( startDate );
return {
from: startDate,
to: endDate,
};
};
/**
* Returns selected date in string format
*
* @param {*} selectedDates
* @return {string} - Formatted string.
*/
export const getSelectedDate = ( selectedDates ) => {
if ( ! selectedDates.from ) {
return '';
}
if ( ! selectedDates.to ) {
return `${ format( selectedDates.from, 'MM/dd/yyyy' ) }`;
}
return `${ format( selectedDates.from, 'MM/dd/yyyy' ) } - ${ format(
selectedDates.to,
'MM/dd/yyyy'
) }`;
};
/**
* Get the date placeholder
*
* @param {number} days - The number of days to get the date range for.
* @param {boolean} [includeToday=true] - Whether to include today in the date range.
* @return {string} - The formatted date string.
*/
export const getDatePlaceholder = ( days = 30, includeToday = true ) => {
const endDate = includeToday ? startOfToday() : startOfYesterday();
let startDate = new Date( endDate );
// When calculating for N days, we need to go back (N-1) days from the end date
// to include the end date itself in the count
startDate.setDate( endDate.getDate() - days );
startDate = startOfDay( startDate );
const formattedStartDate = formatDate( startDate, 'MM/dd/yyyy' );
const formattedEndDate = formatDate( endDate, 'MM/dd/yyyy' );
return `${ formattedStartDate } - ${ formattedEndDate }`;
};
/**
* Format number to k, m, b, t, etc.
*
* @param {number} number - The number to format.
* @param {Object} options - Formatting options.
* @param {number} [options.decimals=1] - Number of decimal places to show.
* @param {boolean} [options.forceDecimals=false] - Whether to always show decimals.
* @return {string} - The formatted number.
*/
export const formatNumber = ( number, options = {} ) => {
const { decimals = 1, forceDecimals = false } = options;
// Handle non-numeric inputs
if ( typeof number !== 'number' || isNaN( number ) ) {
return '0';
}
// Handle negative numbers
const isNegative = number < 0;
const absoluteNumber = Math.abs( number );
// Return small numbers as is
if ( absoluteNumber < 1000 ) {
return isNegative ? `-${ absoluteNumber }` : absoluteNumber.toString();
}
const suffixes = [
{ value: 1e3, suffix: 'k' },
{ value: 1e6, suffix: 'm' },
{ value: 1e9, suffix: 'b' },
{ value: 1e12, suffix: 't' },
{ value: 1e15, suffix: 'p' },
{ value: 1e18, suffix: 'e' },
{ value: 1e21, suffix: 'z' },
{ value: 1e24, suffix: 'y' },
{ value: 1e27, suffix: 'r' },
{ value: 1e30, suffix: 'q' },
];
// Find the appropriate suffix
const suffix =
suffixes.find( ( { value } ) => absoluteNumber < value * 1000 ) ||
suffixes[ suffixes.length - 1 ];
// Calculate the formatted number
const formattedValue = ( absoluteNumber / suffix.value ).toFixed(
decimals
);
// Remove trailing zeros if forceDecimals is false
const finalValue = forceDecimals
? formattedValue
: formattedValue.replace( /\.?0+$/, '' );
return `${ isNegative ? '-' : '' }${ finalValue }${ suffix.suffix }`;
};
/**
* Convert dates to ISO strings while preserving the local date
*
* @param {string|Date} date - The date to convert.
* @return {string} - The ISO string.
*/
export const formatToISOPreserveDate = ( date ) => {
const d = new Date( date );
// Create ISO string with local timezone offset to preserve the date
return new Date(
d.getTime() - d.getTimezoneOffset() * 60000
).toISOString();
};
/**
* Format a date based on its position in a date range
* 1. If the same year is selected across the range, exclude yyyy
* 2. If date of same month is selected across the range, exclude MMM and yyyy
*
* @param {string|Date} date - The date to format
* @param {string|Date} from - The start date of the range
* @param {string|Date} start - The end date of the range
* @param {string} dateFormat - The format to use for the full date (default: 'MMM dd, yyyy')
* @return {string} - Formatted date string
*/
export const formatDateRange = (
date,
from,
start,
dateFormat = 'MMM dd, yyyy'
) => {
if ( ! date ) {
return '';
}
const dateObj = new Date( date );
const fromObj = from ? new Date( from ) : null;
const startObj = start ? new Date( start ) : null;
// Check for invalid date
if ( isNaN( dateObj.getTime() ) ) {
return __( 'Invalid Date', 'surerank' );
}
// If no range is provided, format with the full date format
if ( ! fromObj || ! startObj ) {
return format( dateObj, dateFormat );
}
// Same month and year across the range
if (
fromObj.getMonth() === startObj.getMonth() &&
fromObj.getFullYear() === startObj.getFullYear()
) {
// Just return day for dates in this range
return format( dateObj, 'dd' );
}
// Same year across the range
if ( fromObj.getFullYear() === startObj.getFullYear() ) {
// Return month and day without year
return format( dateObj, 'MMM dd' );
}
// Different years - show full date
return format( dateObj, dateFormat );
};
/**
* Debounce a function
*
* @param {Function} func - The function to debounce
* @param {number} timeout - The timeout in milliseconds
* @return {Function} - The debounced function
*/
export const debounce = ( func, timeout = 400 ) => {
let timer;
return ( ...args ) => {
clearTimeout( timer );
timer = setTimeout( () => {
func.apply( this, args );
}, timeout );
};
};
/**
* Create a resource promise
*
* @param {Promise} promise - The promise to create a resource for
* @return {Object} - The resource promise
*/
export const createResourcePromise = ( promise ) => {
let status = 'pending';
let result;
const suspender = promise.then(
( data ) => {
status = 'success';
result = data;
},
( error ) => {
status = 'error';
result = error;
}
);
return {
read() {
if ( status === 'pending' ) {
throw suspender;
} else if ( status === 'error' ) {
throw result;
} else if ( status === 'success' ) {
return result;
}
},
};
};
/**
* Decode HTML entities in a string
*
* @param {string} text - The text to decode
* @return {string} - The decoded text
*/
export const decodeHtmlEntities = ( text ) => {
if ( ! text || typeof text !== 'string' ) {
return text;
}
const parser = new DOMParser();
const doc = parser.parseFromString( text, 'text/html' );
return doc.documentElement.textContent ?? text;
};
/**
* Check if a string is a valid URL
*
* @param {string} string - The string to check
* @return {boolean} - True if the string is a valid URL, otherwise false
*/
export const isURL = ( string ) => {
try {
const urlPattern =
/^(https?:\/\/)?((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|localhost|\d{1,3}(\.\d{1,3}){3})(:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?(\s.*)?$/i;
return urlPattern.test( string );
} catch ( error ) {
return false;
}
};
/**
* Format the bad, fair, and passed checks for the SEO checks.
*
* @param {Object} seoScore - The SEO score object containing the checks.
* @return {Object} - An object containing the formatted checks.
*/
export const formatSeoChecks = ( seoScore ) => {
if ( ! seoScore ) {
return [];
}
return Object.entries( seoScore ).map( ( [ key, check ] ) => {
const title = key
.replace( /_/g, ' ' )
.replace( /\b\w/g, ( c ) => c.toUpperCase() );
return {
...check,
id: key,
title: check?.message || title,
data: check?.description,
showImages: key === 'image_alt_text',
};
} );
};
/**
* Get the categorized checks.
*
* @param {Array} checks - The checks to categorize.
* @param {Array} ignoredList - The ignored list.
* @return {Object} - The categorized checks.
*/
export const getCategorizedChecks = ( checks, ignoredList = [] ) => {
return checks.filter( Boolean ).reduce(
( acc, check ) => {
// Check if the check is in the ignored list
if ( ignoredList.includes( check.id ) ) {
check.ignore = true;
acc.ignoredChecks.push( check );
} else {
// set the flag to false to show the check in the UI
check.ignore = false;
if ( check.status === 'error' ) {
acc.badChecks.push( check );
} else if ( check.status === 'warning' ) {
acc.fairChecks.push( check );
} else if ( check.status === 'suggestion' ) {
acc.suggestionChecks.push( check );
} else if ( check.status === 'success' ) {
acc.passedChecks.push( check );
}
}
return acc;
},
{
badChecks: [],
fairChecks: [],
suggestionChecks: [],
passedChecks: [],
ignoredChecks: [],
}
);
};
export const getSeoCheckLabel = ( type, counts ) => {
if ( type === 'error' ) {
return sprintf(
// translators: %1$s is the number of issues detected, %2$s is the word "Issue".
'%1$s %2$s Detected',
counts,
_n( 'Issue', 'Issues', counts, 'surerank' )
);
}
if ( type === 'warning' ) {
return sprintf(
// translators: %1$s is the number of issues detected, %2$s is the word "Issue".
'%1$s %2$s Detected',
counts,
_n( 'Warning', 'Warnings', counts, 'surerank' )
);
}
return __( 'SEO is Optimized', 'surerank' );
};
/**
* Get status indicator CSS classes based on check status
*
* @param {string} status Status of page checks ('error', 'warning', 'suggestion', 'success')
* @return {string} CSS classes for the status indicator
*/
export const getStatusIndicatorClasses = ( status ) => {
switch ( status ) {
case 'error':
return 'bg-support-error';
case 'warning':
return 'bg-support-warning';
case 'suggestion':
return 'bg-support-info';
case 'success':
return 'bg-support-success';
default:
return 'bg-background-secondary';
}
};
/**
* Get accessibility label for status indicator
*
* @param {number} errorAndWarnings Count of errors and warnings
* @return {string} Accessibility label text
*/
export const getStatusIndicatorAriaLabel = ( errorAndWarnings ) => {
if ( errorAndWarnings > 0 ) {
return sprintf(
/* translators: %1$d: number of errors and warnings */
__( '%1$d %2$s need attention.', 'surerank' ),
errorAndWarnings,
_n( 'issue', 'issues', errorAndWarnings, 'surerank' )
);
}
return __( 'All SEO checks passed.', 'surerank' );
};
/**
* Extracts URL parameters from a given URL string and returns them as an object.
* Handles both absolute and relative URLs by using the current origin as a base for relative ones.
*
* @param {string} url - The URL string to parse.
* @param {string} [key] - The specific key to retrieve from the URL parameters.
* @return {Object} An object containing the URL parameters, or an empty object if parsing fails.
*/
export const getURLParams = ( url, key = '' ) => {
try {
const fullUrl = new URL( url, window.location.origin );
const params = fullUrl.searchParams;
if ( key ) {
return params.get( key ) || '';
}
return Object.fromEntries( params.entries() );
} catch ( error ) {
return key ? '' : {};
}
};
/**
* Removes specified query parameters from a given URL string and returns the updated URL.
* Handles both absolute and relative URLs by using the current origin as a base for relative ones.
* If an array of keys is provided, removes all matching parameters; otherwise, removes the single key.
*
* @param {string} url - The URL string to modify.
* @param {string|string[]} keys - The key(s) of the query parameter(s) to remove.
* @return {string} The updated URL string with the specified parameters removed, or the original URL if parsing fails.
*/
export const removeQueryParams = ( url, keys ) => {
try {
const fullUrl = new URL( url, window.location.origin );
const params = fullUrl.searchParams;
if ( Array.isArray( keys ) ) {
keys.forEach( ( key ) => params.delete( key ) );
} else {
params.delete( keys );
}
return fullUrl.toString();
} catch ( error ) {
return url;
}
};
/**
* Adds category property to each check item in the response
*
* @param {Object} response - The API response object
* @param {string} category - The category to add to each item
* @return {Object} The response object with category added to each item
*/
export const addCategoryToSiteSeoChecks = ( response, category ) => {
if ( response && typeof response === 'object' ) {
Object.keys( response ).forEach( ( key ) => {
if ( response[ key ] && typeof response[ key ] === 'object' ) {
response[ key ].category = category;
}
} );
}
return response;
};