/**
* WordPress dependencies
*/
import { useDispatch, useSelect } from '@safe-wordpress/data';
import { useCallback, useEffect } from '@safe-wordpress/element';
import { _x } from '@safe-wordpress/i18n';
/**
* External dependencies
*/
import clsx from 'clsx';
import { CodeEditor } from '@nab/components';
import { store as NAB_DATA } from '@nab/data';
import { store as NAB_EDITOR } from '@nab/editor';
import {
getValue,
omitUndefineds,
setValue as setValueInLocalStore,
} from '@nab/utils';
import type { AlternativeId, CssEditorState } from '@nab/types';
/**
* Internal dependencies
*/
import { VisualEditor } from '../visual-editor';
import { ContentEditor } from '../content-editor';
import type { AlternativeAttributes } from '../../../../../../../../../packages/experiment-library/css/types';
export type BodyProps = {
readonly alternativeId: AlternativeId;
};
export function Body( { alternativeId }: BodyProps ): JSX.Element {
const activeSelector = useActiveSelector();
const [ css, setCss ] = useCssValue( alternativeId );
const [ editor, setEditor ] = useEditor(
alternativeId,
getPreferredEditor()
);
const markAsActive = useCssEditorActivation();
const highlightSelectedItems = useSelectorHighlighter();
return (
{ ( tab: Tab ): JSX.Element => {
switch ( tab.name ) {
case 'css-editor':
return (
markAsActive( true ) }
onBlur={ () => markAsActive( false ) }
/>
);
case 'visual-editor':
return ;
case 'content-editor':
return ;
}
} }
);
}
// ============
// HELPER VIEWS
// ============
type TabPanelProps = {
readonly className: string;
readonly activeClass: string;
readonly orientation: 'horizontal';
readonly onSelect: ( tab: EditorMode ) => void;
readonly children: ( tab: Tab ) => JSX.Element;
readonly activeTab: EditorMode;
readonly tabs: Readonly< [ Tab, ...Tab[] ] >;
};
// NOTICE. This is a workaround until “TabPanel” can be controlled/uncontrolled.
const TabPanel = ( {
className,
activeClass,
orientation,
onSelect,
children,
activeTab,
tabs,
}: TabPanelProps ) => (
{ tabs.map( ( tab ) => (
) ) }
{ children(
tabs.find( ( t ) => t.name === activeTab ) || tabs[ 0 ]
) }
);
// =====
// HOOKS
// =====
const useCssValue = ( alternativeId: AlternativeId ) => {
const alternative = useSelect(
( select ) =>
select( NAB_EDITOR ).getAlternative< AlternativeAttributes >(
alternativeId
),
[ alternativeId ]
);
const value = alternative?.attributes?.css || '';
const { setAlternative } = useDispatch( NAB_EDITOR );
const setValue = ( css: string ) => {
if ( ! alternative ) {
return;
}
void setAlternative( alternative.id, {
...alternative,
attributes: omitUndefineds( {
...alternative.attributes,
css,
} ),
} );
};
return [ value, setValue ] as const;
};
const useActiveSelector = () =>
useSelect(
( select ) =>
select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' )
?.activeSelector,
[]
);
function getPreferredEditor(): EditorMode {
const value = getValue( 'cssPreferredEditor' );
const isValid = ( v: unknown | EditorMode ): v is EditorMode =>
TABS.some( ( t ) => t.name === v );
return isValid( value ) ? value : 'visual-editor';
}
function useEditor(
alternativeId: AlternativeId,
defaultEditorMode: EditorMode
): [ EditorMode, ( editor: EditorMode ) => void ] {
const editor = useSelect(
( select ) =>
select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' ),
[]
);
const { setPageAttribute } = useDispatch( NAB_DATA );
const setEditor = useCallback(
( mode: EditorMode ): void => {
setValueInLocalStore( 'cssPreferredEditor', mode );
if ( mode === editor?.mode ) {
return;
}
void setPageAttribute( 'css-editor/cssEditorState', {
isEditorActive: false,
areChangesActive: true,
activeSelector: '',
activeElement: undefined,
contentValues: [],
isPreviewInScope: false,
...editor,
alternativeId,
mode,
details: undefined,
} );
},
[ alternativeId, editor, setPageAttribute ]
);
useEffect( () => {
if ( editor ) {
return;
}
setEditor( defaultEditorMode );
}, [ editor, defaultEditorMode, setEditor ] );
return [ editor?.mode ?? defaultEditorMode, setEditor ];
}
function useCssEditorActivation() {
const editor = useSelect(
( select ) =>
select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' ),
[]
);
const { setPageAttribute } = useDispatch( NAB_DATA );
return ( isEditorActive: boolean ) =>
void (
'css-editor' === editor?.mode &&
setPageAttribute( 'css-editor/cssEditorState', {
...editor,
isEditorActive,
} )
);
}
const useSelectorHighlighter = () => {
const editor = useSelect(
( select ) =>
select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' ),
[]
);
const { setPageAttribute } = useDispatch( NAB_DATA );
if ( ! editor ) {
return () => void null;
}
return ( selectors: ReadonlyArray< string > = [] ) => {
const activeSelector = selectors.join( ', ' );
if ( activeSelector === editor.activeSelector ) {
return;
}
void setPageAttribute( 'css-editor/cssEditorState', {
...editor,
activeSelector,
activeElement: undefined,
} );
};
};
// ====
// DATA
// ====
type EditorMode = CssEditorState[ 'mode' ];
type Tab = {
readonly name: EditorMode;
readonly title: string;
readonly className: string;
};
const TABS: Readonly< [ Tab, Tab, Tab ] > = [
{
name: 'visual-editor',
title: _x( 'Appearance', 'text', 'nelio-ab-testing' ),
className: 'nab-css-editor-sidebar__tab',
},
{
name: 'css-editor',
title: _x( 'CSS', 'text', 'nelio-ab-testing' ),
className: 'nab-css-editor-sidebar__tab',
},
{
name: 'content-editor',
title: _x( 'Content', 'text', 'nelio-ab-testing' ),
className: 'nab-css-editor-sidebar__tab',
},
];