import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import type { DragEvent } from 'react'; export type TemplateToken = { value: string; label: string; description: string; }; type TemplateTokenEditorProps = { label: string; description: string; value: string; availableTokens: TemplateToken[]; onChange: ( next: string ) => void; e2eName?: string; }; const parseTemplateTokens = ( value: string, allowed: Set ) => { const matches = value.match( /%[^%]+%/g ) ?? []; return matches.filter( ( token ) => allowed.has( token ) ); }; const serializeTemplateTokens = ( tokens: string[] ) => tokens.length > 0 ? tokens.join( ' ' ).trim() : ''; const formatTokenLabel = ( token: string, lookup: Map ) => lookup.get( token ) ?? token.replace( /%/g, '' ); const formatTokenLocator = ( token: string, lookup: Map ) => formatTokenLabel( token, lookup ).replace( /[^a-zA-Z0-9_-]+/g, '-' ); const TemplateTokenEditor = ( { label, description, value, availableTokens, onChange, e2eName, }: TemplateTokenEditorProps ) => { const allowedTokens = useMemo( () => new Set( availableTokens.map( ( token ) => token.value ) ), [ availableTokens ], ); const tokenLabelMap = useMemo( () => new Map( availableTokens.map( ( token ) => [ token.value, token.label ] ), ), [ availableTokens ], ); const tokens = parseTemplateTokens( value, allowedTokens ); const updateTokens = ( nextTokens: string[] ) => { onChange( serializeTemplateTokens( nextTokens ) ); }; const handleAddToken = ( token: string ) => { updateTokens( [ ...tokens, token ] ); }; const moveTokenTo = ( fromIndex: number, toIndex: number ) => { if ( fromIndex === toIndex || fromIndex + 1 === toIndex ) { return; } const nextTokens = [ ...tokens ]; const [ moved ] = nextTokens.splice( fromIndex, 1 ); const normalizedIndex = fromIndex < toIndex ? toIndex - 1 : toIndex; nextTokens.splice( normalizedIndex, 0, moved ); updateTokens( nextTokens ); }; const handleRemoveToken = ( index: number ) => { const nextTokens = [ ...tokens ]; nextTokens.splice( index, 1 ); updateTokens( nextTokens ); }; const handleDrop = ( event: DragEvent, insertIndex?: number, ) => { event.preventDefault(); const payload = event.dataTransfer.getData( 'text/plain' ); if ( payload.startsWith( 'index:' ) ) { const index = Number( payload.slice( 6 ) ); if ( Number.isNaN( index ) ) { return; } const destination = typeof insertIndex === 'number' ? insertIndex : tokens.length; moveTokenTo( index, destination ); return; } if ( allowedTokens.has( payload ) ) { const nextTokens = [ ...tokens ]; if ( typeof insertIndex === 'number' ) { nextTokens.splice( insertIndex, 0, payload ); } else { nextTokens.push( payload ); } updateTokens( nextTokens ); } }; const handleTokenDrop = ( event: DragEvent, targetIndex: number, ) => { const rect = event.currentTarget.getBoundingClientRect(); const centerX = rect.left + ( rect.width / 2 ); const dropAfter = event.clientX > centerX; const insertIndex = dropAfter ? targetIndex + 1 : targetIndex; handleDrop( event, insertIndex ); }; const handleDragOver = ( event: DragEvent ) => { event.preventDefault(); }; return (

{ label }

{ description }

handleDrop( event ) } > { tokens.length > 0 ? ( tokens.map( ( token, index ) => ( event.dataTransfer.setData( 'text/plain', `index:${ index }`, ) } onDragOver={ handleDragOver } onDrop={ ( event ) => handleTokenDrop( event, index ) } > { formatTokenLabel( token, tokenLabelMap ) }
handleRemoveToken( index ) } onKeyDown={ ( event ) => { if ( event.key === 'Enter' || event.key === ' ' ) { event.preventDefault(); handleRemoveToken( index ); } } } > x
) ) ) : ( { __( 'Drag tokens here to build the template.', 'airygen-seo' ) } ) }
{ availableTokens.map( ( token ) => ( ) ) }

{ __( 'Click or drag tokens into the template. Manual typing is disabled.', 'airygen-seo', ) }

); }; export default TemplateTokenEditor;