Skip to content

Commit

Permalink
Improvements around existing link counting and highlighting.
Browse files Browse the repository at this point in the history
  • Loading branch information
vaurdan committed Feb 19, 2025
1 parent 815990b commit 135ee3e
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@ import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { LinkType, PostLinks } from '../../provider';
import { TrafficBoostStore } from '../../store';
import { HydratedPost } from '../../../../../common/base-wordpress-provider';

/**
* Represents the type of link.
*
* @since 3.18.0
*/
export type LinkType = 'external' | 'internal' | 'smart';

/**
* Represents the links for a post.
*
* @since 3.18.0
*/
export interface PostLinks extends Record<LinkType, HTMLAnchorElement[]> {
total: number;
}

/**
* The shape of the link counter object.
Expand All @@ -27,7 +43,7 @@ type LinkCount = {
* @since 3.18.0
*/
interface LinkCounterProps {
postLinks: PostLinks;
post: HydratedPost;
onLinkTypeClick?: ( type: LinkType | null ) => void;
selectedLinkType: LinkType | null;
}
Expand All @@ -40,7 +56,7 @@ interface LinkCounterProps {
* @param {LinkCounterProps} props The component's props.
*/
export const LinkCounter = ( {
postLinks,
post,
onLinkTypeClick,
selectedLinkType: initialSelectedLinkType,
}: LinkCounterProps ): React.JSX.Element => {
Expand All @@ -53,6 +69,35 @@ export const LinkCounter = ( {

const { setPreviewLinkType } = useDispatch( TrafficBoostStore );

useEffect( () => {
const postContent = post.content.raw;
const siteUrl = new URL( post.link ).hostname;

// Create a new DOMParser instance.
const parser = new DOMParser();
const doc = parser.parseFromString( postContent, 'text/html' );
const allLinks = doc.querySelectorAll( 'a' );

// Filter out links that have no text.
const linksWithText = Array.from( allLinks ).filter( ( link ) => link.textContent?.trim() !== '' );

// Classify the links into external, internal, and smart.
// Smart links contain the data-smartlink attribute.
const smartLinks = linksWithText.filter( ( link ) => link.hasAttribute( 'data-smartlink' ) );

// Internal links contain the site URL in the href attribute.
const internalLinks = linksWithText.filter( ( link ) => link.href.includes( siteUrl ) );

// External links are links that do not contain the site URL in the href attribute.
const externalLinks = linksWithText.filter( ( link ) => ! link.href.includes( siteUrl ) );

setLinks( {
external: externalLinks.length,
internal: internalLinks.length,
smart: smartLinks.length,
} );
}, [ post ] );

/**
* Sets the selected link type and preview link type when the initial selected link type changes.
*
Expand All @@ -63,21 +108,6 @@ export const LinkCounter = ( {
setPreviewLinkType( initialSelectedLinkType );
}, [ initialSelectedLinkType, setPreviewLinkType ] );

/**
* Updates the link counts when postLinks changes.
*
* @since 3.18.0
*/
useEffect( () => {
const newLinks = {
external: postLinks.external.length,
internal: postLinks.internal.length,
smart: postLinks.smart.length,
};

setLinks( newLinks );
}, [ postLinks ] );

/**
* Handles click events on link type buttons.
*
Expand Down Expand Up @@ -110,15 +140,17 @@ export const LinkCounter = ( {
*/
const isSelected = ( type: LinkType ) => selectedLinkType === type;

const totalLinks = links.external + links.internal + links.smart;

return (
<div className="traffic-boost-preview-info-links">
<div className="traffic-boost-preview-info-links-summary">
{ postLinks.total > 0 ? (
{ totalLinks > 0 ? (
<>
{ sprintf(
/* translators: %d: number of outbound links */
__( 'Contains %d outbound links:', 'wp-parsely' ),
postLinks.total
totalLinks
) }
</>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const PreviewHeader = ( {
<div dangerouslySetInnerHTML={ { __html: activeLink?.targetPost?.title.rendered } } />
</div>
<LinkCounter
postLinks={ activeLink.postLinks }
post={ activeLink.targetPost }
selectedLinkType={ null }
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ export const PreviewIframe = ( {
onLoadingChange,
onRestoreOriginal,
}: PreviewIframeProps ): React.JSX.Element => {
const contentAreaRef = useRef<Element | null>( null );
const [ messageIndex, setMessageIndex ] = useState<number>( 0 );

const contentAreaRef = useRef<Element | null>( null ) as React.MutableRefObject<Element | null>;
const iframeRef = useRef<HTMLIFrameElement>( null );
const isInboundLink = ! activeLink?.isSuggestion;

Expand All @@ -81,7 +81,7 @@ export const PreviewIframe = ( {
*/
useEffect( () => {
setMessageIndex( Math.floor( Math.random() * messages.length ) );
}, [ activeLink, messages ] );
}, [ isGenerating, isLoading, activeLink, messages ] );

/**
* Highlights the smart link in the iframe.
Expand Down Expand Up @@ -203,7 +203,7 @@ export const PreviewIframe = ( {
event.preventDefault();
event.stopPropagation();
}, true );
}, [] );
}, [ contentAreaRef ] );

/**
* Jumps to the smart link text in the iframe.
Expand Down Expand Up @@ -285,7 +285,15 @@ export const PreviewIframe = ( {

onLoadingChange( false );
jumpToSmartLink( iframe );
}, [ disableNavigation, hideAdminBar, highlightLinkType, injectHighlightStyles, jumpToSmartLink, onLoadingChange, selectedLinkType ] );
}, [ contentAreaRef,
disableNavigation,
hideAdminBar,
highlightLinkType,
injectHighlightStyles,
jumpToSmartLink,
onLoadingChange,
selectedLinkType,
] );

/**
* Handles iframe initialization and cleanup.
Expand Down Expand Up @@ -331,7 +339,7 @@ export const PreviewIframe = ( {
*/
useEffect( () => {
contentAreaRef.current = null;
}, [ activeLink ] );
}, [ activeLink, contentAreaRef ] );

/**
* Re-highlights smart link when selection changes.
Expand All @@ -346,7 +354,7 @@ export const PreviewIframe = ( {

removeSmartLinkHighlights( iframe );
highlightSmartLink( iframe );
}, [ highlightSmartLink, isLoading, removeSmartLinkHighlights, selectedText ] );
}, [ contentAreaRef, highlightSmartLink, isLoading, removeSmartLinkHighlights, selectedText ] );

/**
* Highlights the link type in the iframe.
Expand All @@ -360,7 +368,7 @@ export const PreviewIframe = ( {
}

highlightLinkType( iframe, selectedLinkType );
}, [ highlightLinkType, iframeRef, isLoading, selectedLinkType ] );
}, [ contentAreaRef, highlightLinkType, iframeRef, isLoading, selectedLinkType ] );

/**
* Picks a random message with interval based on message length when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { useCallback } from '@wordpress/element';
* Internal imports
*/
import { escapeRegExp } from '../../../../../common/utils/functions';
import { LinkType, TrafficBoostLink } from '../../provider';
import { TrafficBoostLink } from '../../provider';
import { TextSelection } from '../preview';
import { LinkType } from '../components/link-counter';

/**
* Props for the useIframeHighlight hook.
Expand Down Expand Up @@ -571,8 +572,9 @@ export const useIframeHighlight = ( {
* @param {string} selectedLinkType The selected link type to highlight.
*/
const highlightLinkType = useCallback( ( iframe: HTMLIFrameElement, selectedLinkType: LinkType | null ) => {
const contentArea = contentAreaRef.current;
const iframeDocument = iframe.contentDocument ?? iframe.contentWindow?.document;
if ( ! iframeDocument ) {
if ( ! contentArea || ! iframeDocument ) {
return;
}

Expand All @@ -583,33 +585,34 @@ export const useIframeHighlight = ( {
return;
}

// Get all the links of the selected link type.
const links = activeLink?.postLinks[ selectedLinkType ];
const siteUrl = new URL( activeLink.targetPost.link ).hostname;
let links: HTMLAnchorElement[] = Array.from( contentArea.querySelectorAll<HTMLAnchorElement>( 'a' ) );

// Filter out links that don't have text.
links = links.filter( ( link ) => link.textContent?.trim() !== '' );

switch ( selectedLinkType ) {
case 'external':
links = links.filter( ( link ) => ! link.href.includes( siteUrl ) );
break;
case 'internal':
links = links.filter( ( link ) => link.href.includes( siteUrl ) );
break;
case 'smart':
links = links.filter( ( link ) => link.hasAttribute( 'data-smartlink' ) );
break;
}

if ( ! links?.length ) {
return;
}

// Find and highlight matching links in the iframe.
const allIframeLinks = iframeDocument.querySelectorAll( 'a' );
allIframeLinks.forEach( ( iframeLink ) => {
// Match links based on href and text content.
const matchingLink = links.find( ( link ) => {
if ( link.hasAttribute( 'data-smartlink' ) ) {
return link.getAttribute( 'data-smartlink' ) === iframeLink.getAttribute( 'data-smartlink' );
}

const hrefMatches = link.href === iframeLink.href;
const textMatches = link.textContent === iframeLink.textContent;
return hrefMatches && textMatches;
} );

if ( matchingLink ) {
const selectionRange = iframeDocument.createRange();
selectionRange.selectNode( iframeLink );
highlightRange( selectionRange, 'link-type-highlight' );
}
links.forEach( ( link ) => {
const selectionRange = iframeDocument.createRange();
selectionRange.selectNode( link );
highlightRange( selectionRange, 'link-type-highlight' );
} );
}, [ activeLink, highlightRange, removeHighlights ] );
}, [ activeLink, contentAreaRef, highlightRange, removeHighlights ] );

return {
injectHighlightStyles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ export const TrafficBoostPreview = ( {
if ( itemIndex === totalItems && totalItems === 1 ) {
setSelectedTab( TrafficBoostSidebarTabs.INBOUND_LINKS );
setSelectedLink( link );
// Refresh the iframe.
setPreviewUrl( previewUrl + '?cache-bust=' + Date.now() );
} else if ( itemIndex === totalItems ) {
// Navigate to previous suggestion when accepting the last one.
handlePrevious();
Expand Down
Loading

0 comments on commit 135ee3e

Please sign in to comment.