/** * 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 ) );