/**
* WordPress dependencies
*/
import { Button } from '@safe-wordpress/components';
import { useSelect } from '@safe-wordpress/data';
import { useState, useEffect } from '@safe-wordpress/element';
import { _x } from '@safe-wordpress/i18n';
/**
* External dependencies
*/
import createIntroJs from 'intro.js';
import { store as NC_DATA } from '@nelio-content/data';
import { getValue, setValue } from '@nelio-content/utils';
import type { IntroJs, Step as IntroJsStep } from 'intro.js';
import type { Dict, TutorialStep, Walkthrough } from '@nelio-content/types';
/**
* Internal dependencies
*/
import './style.scss';
export type ContextualHelpProps = {
readonly component?: ( props: ComponentProps ) => JSX.Element | null;
readonly context?: string;
readonly walkthrough: Walkthrough;
readonly autostart?: boolean;
readonly delay?: number;
};
type ComponentProps = {
readonly className: string;
readonly runWalkthrough: () => void;
};
export const ContextualHelp = ( {
component,
context = '',
walkthrough = [],
autostart = false,
delay = 250,
}: ContextualHelpProps ): JSX.Element | null => {
const runWalkthrough = useWalkthroughEffect( {
context,
steps: walkthrough,
autostart,
delay,
} );
if ( ! runWalkthrough ) {
return null;
}
const Component = component || DefaultComponent;
return (
);
};
const DefaultComponent = ( { className, runWalkthrough }: ComponentProps ) => (
);
// =====
// HOOKS
// =====
function useWalkthroughEffect( {
context,
steps,
autostart,
delay,
}: {
readonly context: string;
readonly steps: ReadonlyArray< TutorialStep >;
readonly autostart: boolean;
readonly delay: number;
} ): false | ( () => void ) {
const isAutoRunEnabled = useSelect(
( select ) => select( NC_DATA ).areAutoTutorialsEnabled(),
[]
);
const [ isRunning, setRunning ] = useState( false );
useEffect( () => {
if (
( !! steps.length && ! autostart ) ||
! shouldRunOnStartup( context ) ||
! isAutoRunEnabled
) {
return;
}
const timeout = setTimeout( () => setRunning( true ), delay );
return () => clearTimeout( timeout );
} );
useEffect( () => {
if ( ! isRunning ) {
return;
}
setValue( `is-guide-${ context }-disabled`, true );
const lastStep: TutorialStep = {
title: _x( 'Enjoy!', 'user', 'nelio-content' ),
intro: _x(
'That’s all! If you want to view this guide again, please click here.',
'user',
'nelio-content'
),
active: () =>
!! document.querySelector(
`.nelio-content-contextual-help__${ context }-button`
),
element: () =>
document.querySelector(
`.nelio-content-contextual-help__${ context }-button`
),
};
const finalSteps = [ ...steps, lastStep ]
.filter(
( s: TutorialStep ) => ! ( 'active' in s ) || !! s.active?.()
)
.map( tutorialStepToIntroStep );
const walkthrough = createIntroJs().setOptions( {
buttonClass:
'button button-secondary nelio-content-walkthrough-button',
disableInteraction: true,
exitOnOverlayClick: false,
overlayOpacity: 0.8,
tooltipClass: 'nelio-content-walkthrough-tooltip',
steps: finalSteps,
} );
walkthrough.onbeforechange( () => applyClicks( walkthrough ) );
walkthrough.onafterchange( () =>
updateNavigationButtons( walkthrough, finalSteps.length )
);
walkthrough.onexit( () => setRunning( false ) );
walkthrough.start();
}, [ isRunning, context, steps ] );
return !! steps.length && ( () => setRunning( true ) );
}
// =======
// HELPERS
// =======
function tutorialStepToIntroStep(
s: TutorialStep
): IntroJsStep & { extraNelioContent: TutorialStep } {
const element = s.element?.();
return {
intro: s.intro,
title: s.title,
element: element || undefined,
extraNelioContent: s,
};
}
function shouldRunOnStartup( context: string ): boolean {
return ! getValue( `is-guide-${ context }-disabled`, false );
}
function applyClicks( introjs: IntroJs ): void {
if ( ! introjs ) {
return;
}
const index = introjs.currentStep() || -1;
const steps = ( introjs as unknown as Dict< unknown > )
._introItems as ReadonlyArray<
Partial< { extraNelioContent: TutorialStep } >
>;
const step = steps[ index ];
step?.extraNelioContent?.elementToClick?.()?.click();
if ( step?.extraNelioContent?.element ) {
( step as Dict ).element =
step.extraNelioContent.element() || undefined;
}
}
function updateNavigationButtons( introjs: IntroJs, totalSteps: number ): void {
const { prev, next, skip } = getNavigationButtons( introjs );
if ( ! prev || ! next || ! skip ) {
return;
}
prev.textContent = _x( 'Prev', 'command', 'nelio-content' );
prev.style.display = isFirstStep( introjs ) ? 'none' : 'block';
skip.style.display = isFirstStep( introjs ) ? 'block' : 'none';
if ( isLastStep( introjs, totalSteps ) ) {
next.classList.add( 'button-primary' );
next.classList.remove( 'button-secondary' );
next.textContent = _x( 'Close', 'command', 'nelio-content' );
} else if ( isFirstStep( introjs ) ) {
next.classList.add( 'button-primary' );
next.textContent = _x( 'Start', 'command', 'nelio-content' );
} else {
next.classList.remove( 'button-primary' );
next.classList.add( 'button-secondary' );
next.textContent = _x( 'Next', 'command', 'nelio-content' );
}
}
type NavigationButtons = {
readonly prev?: HTMLElement;
readonly next?: HTMLElement;
readonly skip?: HTMLElement;
};
function getNavigationButtons( introjs: IntroJs ): NavigationButtons {
const buttons: ReadonlyArray< HTMLElement > = Array.from(
document.querySelectorAll( '.nelio-content-walkthrough-button' )
);
const prev = buttons[ 0 ];
const next = buttons[ 1 ];
if ( ! prev || ! next ) {
return {};
}
const skip = getSkipButton( introjs, prev );
return { prev, next, skip };
}
function getSkipButton( introjs: IntroJs, prev: HTMLElement ): HTMLElement {
const existing = document.querySelector< HTMLElement >(
'.nelio-content-walkthrough-skip'
);
if ( existing ) {
return existing;
}
const skip = document.createElement( 'a' );
skip.setAttribute( 'role', 'button' );
skip.setAttribute( 'tabindex', '0' );
skip.className = 'button button-secondary nelio-content-walkthrough-skip';
skip.textContent = _x( 'Skip', 'command', 'nelio-content' );
skip.style.float = 'left';
skip.addEventListener( 'click', () => introjs.exit() );
prev.parentNode?.insertBefore( skip, prev );
return skip;
}
function isFirstStep( introjs: IntroJs ): boolean {
return ! introjs.currentStep();
}
function isLastStep( introjs: IntroJs, totalSteps: number ): boolean {
return introjs.currentStep() === totalSteps - 1;
}