| 1 |
/** |
| 2 |
* Multi-Step Forms document sidebar: pagination, tabs, tab titles. |
| 3 |
* Step count comes from `msf-step-break` blocks; meta via `wppb_fb_msf_*`. |
| 4 |
*/ |
| 5 |
import { __ } from '@wordpress/i18n'; |
| 6 |
import { registerPlugin } from '@wordpress/plugins'; |
| 7 |
import { PluginDocumentSettingPanel } from '@wordpress/edit-post'; |
| 8 |
import { ToggleControl, TextControl, Notice } from '@wordpress/components'; |
| 9 |
import { useEntityProp } from '@wordpress/core-data'; |
| 10 |
import { useSelect } from '@wordpress/data'; |
| 11 |
|
| 12 |
import { countStepBoundaries } from '../lib/stepBoundaries'; |
| 13 |
|
| 14 |
// Inner component runs the meta + block-tree hooks. Conditionally rendered by |
| 15 |
// the outer guard so those hooks never fire on non-PB CPTs — and so the hook |
| 16 |
// order is invariant per component (Rules of Hooks): the outer always calls |
| 17 |
// exactly one hook, and the inner always calls its full hook set before any |
| 18 |
// early return. Mirrors the RepeaterScopeGuard split-component pattern. |
| 19 |
const MsfPanelInner = ( { postType } ) => { |
| 20 |
const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' ); |
| 21 |
|
| 22 |
// Step count = (real step boundaries) + 1. Each boundary closes one step |
| 23 |
// and opens the next; the form always has at least one. |
| 24 |
// |
| 25 |
// `countStepBoundaries` filters injected progress bars out FIRST. // them would mean enabling a "Top of Form" bar shifted a |
| 26 |
// leading step-break to index 1, so it stopped looking leading and the step |
| 27 |
// count jumped by one — the tab-title list gained a phantom step. |
| 28 |
const breakCount = useSelect( ( select ) => { |
| 29 |
const editor = select( 'core/block-editor' ); |
| 30 |
if ( ! editor ) return 0; |
| 31 |
|
| 32 |
return countStepBoundaries( editor.getBlocks() ); |
| 33 |
}, [] ); |
| 34 |
|
| 35 |
// All hooks are above this guard, so it gates only the JSX (not hook order). |
| 36 |
if ( ! meta ) return null; |
| 37 |
|
| 38 |
const pagination = meta.wppb_fb_msf_pagination || ''; |
| 39 |
const tabs = meta.wppb_fb_msf_tabs || ''; |
| 40 |
const tabTitles = Array.isArray( meta.wppb_fb_msf_tab_titles ) ? meta.wppb_fb_msf_tab_titles : []; |
| 41 |
const stepCount = breakCount + 1; |
| 42 |
|
| 43 |
const setTitle = ( i, val ) => { |
| 44 |
const next = tabTitles.slice( 0, stepCount ); |
| 45 |
while ( next.length < stepCount ) next.push( '' ); |
| 46 |
next[ i ] = val; |
| 47 |
setMeta( { wppb_fb_msf_tab_titles: next } ); |
| 48 |
}; |
| 49 |
|
| 50 |
return ( |
| 51 |
<PluginDocumentSettingPanel |
| 52 |
name="wppb-msf-settings" |
| 53 |
title={ __( 'Multi-Step Forms', 'profile-builder' ) } |
| 54 |
> |
| 55 |
{ breakCount === 0 && ( |
| 56 |
<Notice status="info" isDismissible={ false }> |
| 57 |
{ __( 'Insert a "Step Break" block between fields to split this form into steps. Pagination and tabs become active once at least one step break is in place.', 'profile-builder' ) } |
| 58 |
</Notice> |
| 59 |
) } |
| 60 |
|
| 61 |
<ToggleControl |
| 62 |
label={ __( 'Show pagination', 'profile-builder' ) } |
| 63 |
checked={ pagination === 'yes' } |
| 64 |
onChange={ ( on ) => setMeta( { wppb_fb_msf_pagination: on ? 'yes' : 'no' } ) } |
| 65 |
help={ __( 'Display numeric step pagination next to the Previous / Next buttons.', 'profile-builder' ) } |
| 66 |
/> |
| 67 |
|
| 68 |
<ToggleControl |
| 69 |
label={ __( 'Show step tabs', 'profile-builder' ) } |
| 70 |
checked={ tabs === 'yes' } |
| 71 |
onChange={ ( on ) => setMeta( { wppb_fb_msf_tabs: on ? 'yes' : 'no' } ) } |
| 72 |
help={ __( 'Display a tabbed step header above the form. Tab labels are configurable below.', 'profile-builder' ) } |
| 73 |
/> |
| 74 |
|
| 75 |
{ tabs === 'yes' && breakCount > 0 && ( |
| 76 |
<div style={ { marginTop: '12px' } }> |
| 77 |
<p style={ { fontWeight: 600, marginBottom: '8px' } }> |
| 78 |
{ __( 'Tab Titles', 'profile-builder' ) } |
| 79 |
</p> |
| 80 |
{ Array.from( { length: stepCount } ).map( ( _, i ) => ( |
| 81 |
<TextControl |
| 82 |
key={ i } |
| 83 |
label={ __( 'Step', 'profile-builder' ) + ' ' + ( i + 1 ) } |
| 84 |
value={ tabTitles[ i ] || '' } |
| 85 |
onChange={ ( val ) => setTitle( i, val ) } |
| 86 |
placeholder={ __( 'Step', 'profile-builder' ) + ' ' + ( i + 1 ) } |
| 87 |
/> |
| 88 |
) ) } |
| 89 |
</div> |
| 90 |
) } |
| 91 |
</PluginDocumentSettingPanel> |
| 92 |
); |
| 93 |
}; |
| 94 |
|
| 95 |
const MsfPanel = () => { |
| 96 |
const postType = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostType(), [] ); |
| 97 |
if ( ! [ 'wppb-rf-cpt', 'wppb-epf-cpt' ].includes( postType ) ) return null; |
| 98 |
// Add-on bridge must be present (`window.wppbFb.msf`) or REST drops the meta. |
| 99 |
if ( ! ( window.wppbFb && window.wppbFb.msf ) ) return null; |
| 100 |
return <MsfPanelInner postType={ postType } />; |
| 101 |
}; |
| 102 |
|
| 103 |
registerPlugin( 'wppb-msf-settings-plugin', { render: MsfPanel } ); |
| 104 |
|