/**
* WordPress dependencies
*/
import {
Button,
DropdownMenu,
Modal,
Path,
SelectControl,
SVG,
TextControl,
} from '@safe-wordpress/components';
import { useEffect, useState } from '@safe-wordpress/element';
import { _x, sprintf } from '@safe-wordpress/i18n';
import { addQueryArgs, getQueryArg } from '@safe-wordpress/url';
/**
* External dependencies
*/
import { findIndex, noop } from 'lodash';
import { usePageAttribute, usePluginSetting } from '@nab/data';
import { getLetter } from '@nab/utils';
import type {
Alternative,
AlternativeId,
Maybe,
CssSelectorFinderState as State,
} from '@nab/types';
/**
* Internal dependencies
*/
import './style.scss';
export type CssSelectorFinderProps = {
readonly initialUrl: string;
readonly initialMode: State[ 'mode' ];
readonly initialValue: string;
readonly alternatives: ReadonlyArray< Alternative >;
readonly onClose?: () => void;
readonly onAccept?: ( mode: State[ 'mode' ], val: string ) => void;
};
export const CssSelectorFinder = ( {
initialUrl,
initialMode,
initialValue,
...props
}: CssSelectorFinderProps ): JSX.Element => {
const [ state, setState ] = useCssFinderState( initialMode, initialValue );
const validationMessage = useValidationMessage( state );
return (
v && setState( v ) }
validationMessage={ validationMessage }
{ ...props }
/>
) as unknown as string
}
onRequestClose={ noop }
isDismissible={ false }
style={ {
width: '100%',
height: '100%',
padding: 0,
} }
>
);
};
type TitleProps = Omit<
CssSelectorFinderProps,
'initialValue' | 'initialMode'
> & {
readonly state: State;
readonly setState: ( s?: State ) => void;
readonly validationMessage?: string;
};
const Title = ( {
alternatives,
initialUrl,
state,
setState,
validationMessage,
onClose = noop,
onAccept = noop,
}: TitleProps ): JSX.Element => {
const options = alternatives.map( ( { id }, i ) => ( {
label: getLabel( i ),
value: id,
} ) );
const { mode, alternative, value, interactionMode } = state;
const isHovering = 'navigation' !== interactionMode;
const placeholder = ( (): string => {
switch ( mode ) {
case 'id':
return _x( 'Type an element ID…', 'user', 'nelio-ab-testing' );
case 'class':
return _x( 'Type a class name…', 'user', 'nelio-ab-testing' );
case 'css':
return isHovering
? _x(
'Click on an element or type a CSS selector…',
'user',
'nelio-ab-testing'
)
: _x( 'Type a CSS selector…', 'user', 'nelio-ab-testing' );
}
} )();
const loadAlternative = ( id: AlternativeId ) => {
const index = findIndex( options, { value: id } );
setState( {
...state,
alternative: index,
initialUrl: alternatives[ index ]?.links.preview,
} );
};
const homeUrl = usePreviewHomeUrl( state.initialUrl || initialUrl );
const loadHome = homeUrl
? () =>
setState( {
...state,
initialUrl: addQueryArgs( homeUrl, {
'nab-css-selector-finder': true,
'nab-home':
'1' ===
getQueryArg( state?.initialUrl ?? '', 'nab-home' )
? 2
: 1,
} ),
} )
: undefined;
const setValue = ( val: string ) =>
setState( {
...state,
value: val,
} );
const cleanState = () => setState( undefined );
return (
{ !! loadHome && (
) }
{ mode === 'css' && (
setState( {
...state,
interactionMode: 'navigation',
} ),
},
{
title: _x(
'Select an element',
'user',
'nelio-ab-testing'
),
isActive: isHovering,
icon: SingleInspectorIcon,
onClick: () =>
setState( {
...state,
interactionMode: 'single-inspector',
} ),
},
] }
/>
) }
);
};
const SingleInspectorIcon = (): JSX.Element => (
);
const NavigationIcon = (): JSX.Element => (
);
// =====
// HOOKS
// =====
const useCssFinderState = (
initialMode: State[ 'mode' ],
initialValue: string
) => {
const [ init, setInit ] = useState( { mode: 'css', value: '' } );
const attrName = 'css-selector/cssSelectorFinderState';
const [ state, setState ] = usePageAttribute( attrName, {
alternative: 0,
mode: 'css',
value: '',
interactionMode: 'single-inspector',
breadcrumbs: [],
} );
useEffect( () => {
if ( init.mode !== initialMode || init.value !== initialValue ) {
setState( {
alternative: 0,
mode: initialMode,
value: initialValue,
interactionMode:
'css' === initialMode ? 'single-inspector' : 'navigation',
breadcrumbs: [],
} );
setInit( { mode: initialMode, value: initialValue } );
}
}, [ init, initialMode, initialValue, setInit, setState ] );
return [ state, setState ] as const;
};
const useValidationMessage = ( state: State ): Maybe< string > => {
if ( state.numberOfElements === undefined || state.numberOfElements ) {
return undefined;
}
if ( ! state.value?.trim() ) {
return undefined;
}
switch ( state.mode ) {
case 'id':
return _x( 'Element ID not found', 'text', 'nelio-ab-testing' );
case 'class':
return _x( 'Class name not found', 'text', 'nelio-ab-testing' );
case 'css':
return _x(
'No elements match the current CSS selector',
'text',
'nelio-ab-testing'
);
}
};
const usePreviewHomeUrl = ( initialUrl: Maybe< string > ): Maybe< string > => {
const homeUrl = usePluginSetting( 'homeUrl' );
if ( ! initialUrl ) {
return undefined;
}
const experiment = getQueryArg( initialUrl, 'experiment' );
const alternative = getQueryArg( initialUrl, 'alternative' );
const timestamp = getQueryArg( initialUrl, 'timestamp' );
const nabnonce = getQueryArg( initialUrl, 'nabnonce' );
if ( ! experiment || ! alternative || ! timestamp || ! nabnonce ) {
return undefined;
}
return addQueryArgs( homeUrl, {
'nab-preview': true,
experiment,
alternative,
timestamp,
nabnonce,
} );
};
const getLabel = ( index: number ): string =>
sprintf(
/* translators: %s: Letter, such as A, B, or C. */
_x( 'Variant %s', 'text', 'nelio-ab-testing' ),
getLetter( index )
);