/**
* Progress Bar document sidebar panel + live canvas reconciler.
* Gated on `window.wppbFb.progressBar.active`.
*/
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { SelectControl, ToggleControl } from '@wordpress/components';
import { useEntityProp } from '@wordpress/core-data';
import { useSelect, useDispatch, select as dataSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
import {
PROGRESS_BAR_BLOCK,
isStepBoundary,
withoutProgressBars,
} from '../lib/stepBoundaries';
/**
* Desired bar slots for position setting. Uses shared `isStepBoundary()`.
* Returns `{ context, before }` (`before` null = append).
*/
function computeDesiredPlan( nonBarBlocks, position ) {
const wantTop = position === 'Top of Form' || position === 'Both';
const wantBottom = position === 'Bottom of Form' || position === 'Both';
const plan = [];
if ( wantTop ) {
const beforeId = nonBarBlocks.length > 0 ? nonBarBlocks[ 0 ].clientId : null;
plan.push( { context: 'top', before: beforeId } );
}
for ( let i = 0; i < nonBarBlocks.length; i++ ) {
const b = nonBarBlocks[ i ];
if ( ! isStepBoundary( nonBarBlocks, i ) ) continue;
if ( wantBottom ) {
plan.push( { context: 'bottom', before: b.clientId } );
}
if ( wantTop ) {
const nextId = ( i + 1 < nonBarBlocks.length ) ? nonBarBlocks[ i + 1 ].clientId : null;
plan.push( { context: 'top', before: nextId } );
}
}
if ( wantBottom ) {
plan.push( { context: 'bottom', before: null } );
}
return plan;
}
/**
* Pair existing bars to plan slots by matching `position-context`.
* Context changes are remove + insert. Returns `{ removes, inserts }`.
*/
function diffPlan( allBlocks, plan ) {
const removes = [];
const inserts = [];
let planIdx = 0;
const canvasItems = allBlocks.map( ( b ) => ( {
clientId: b.clientId,
name: b.name,
context: b.attributes && b.attributes[ 'position-context' ],
} ) );
const findBarsBefore = ( startIdx ) => {
const out = [];
for ( let j = startIdx - 1; j >= 0; j-- ) {
if ( canvasItems[ j ].name !== PROGRESS_BAR_BLOCK ) break;
out.unshift( canvasItems[ j ] );
}
return out;
};
const consumedBars = new Set();
for ( ; planIdx < plan.length; planIdx++ ) {
const slot = plan[ planIdx ];
if ( slot.before === null ) break;
const anchorIdx = canvasItems.findIndex( ( c ) => c.clientId === slot.before );
if ( anchorIdx < 0 ) {
inserts.push( { context: slot.context, beforeNonBarClientId: slot.before } );
continue;
}
const candidateBars = findBarsBefore( anchorIdx ).filter( ( c ) => ! consumedBars.has( c.clientId ) );
const match = candidateBars.find( ( c ) => c.context === slot.context );
if ( match ) {
consumedBars.add( match.clientId );
} else {
inserts.push( { context: slot.context, beforeNonBarClientId: slot.before } );
}
}
const endSlots = plan.slice( planIdx );
if ( endSlots.length > 0 ) {
let lastNonBarIdx = -1;
for ( let i = canvasItems.length - 1; i >= 0; i-- ) {
if ( canvasItems[ i ].name !== PROGRESS_BAR_BLOCK ) { lastNonBarIdx = i; break; }
}
const trailingBars = [];
for ( let i = lastNonBarIdx + 1; i < canvasItems.length; i++ ) {
if ( canvasItems[ i ].name === PROGRESS_BAR_BLOCK && ! consumedBars.has( canvasItems[ i ].clientId ) ) {
trailingBars.push( canvasItems[ i ] );
}
}
for ( const slot of endSlots ) {
const match = trailingBars.find( ( c ) => c.context === slot.context && ! consumedBars.has( c.clientId ) );
if ( match ) {
consumedBars.add( match.clientId );
} else {
inserts.push( { context: slot.context, beforeNonBarClientId: null } );
}
}
}
for ( const c of canvasItems ) {
if ( c.name === PROGRESS_BAR_BLOCK && ! consumedBars.has( c.clientId ) ) {
removes.push( c.clientId );
}
}
return { removes, inserts };
}
/**
* Keep canvas bars in sync with settings via positional diff.
* Fingerprint ignores bars so our own mutations don't loop.
*/
function useProgressBarReconciler( enabled, position ) {
const treeFingerprint = useSelect(
( select ) => withoutProgressBars( select( 'core/block-editor' ).getBlocks() )
.map( ( b ) => `${ b.name }:${ b.clientId }` )
.join( '|' ),
[]
);
const { insertBlock, removeBlock, updateBlockAttributes } = useDispatch( 'core/block-editor' );
useEffect( () => {
// Unlock first — `lock.remove` makes plain removeBlock a no-op.
const removeLockedBlock = ( clientId ) => {
updateBlockAttributes( clientId, { lock: {} } );
removeBlock( clientId, false );
};
const allBlocks = dataSelect( 'core/block-editor' ).getBlocks();
const nonBar = withoutProgressBars( allBlocks );
if ( ! enabled ) {
for ( const b of allBlocks ) {
if ( b.name === PROGRESS_BAR_BLOCK ) removeLockedBlock( b.clientId );
}
return;
}
const plan = computeDesiredPlan( nonBar, position );
const { removes, inserts } = diffPlan( allBlocks, plan );
for ( const clientId of removes ) {
removeLockedBlock( clientId );
}
for ( const slot of inserts ) {
const currentBlocks = dataSelect( 'core/block-editor' ).getBlocks();
let idx = currentBlocks.length;
if ( slot.beforeNonBarClientId !== null ) {
idx = currentBlocks.findIndex( ( b ) => b.clientId === slot.beforeNonBarClientId );
if ( idx < 0 ) idx = currentBlocks.length;
}
insertBlock(
createBlock( PROGRESS_BAR_BLOCK, {
'position-context': slot.context,
lock: { move: true, remove: true },
} ),
idx,
undefined,
false
);
}
}, [ enabled, position, treeFingerprint, insertBlock, removeBlock, updateBlockAttributes ] );
}
/**
* Split across components so hook count never changes when meta resolves late.
*/
const ProgressBarPanel = () => {
const postType = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostType(), [] );
if ( ! [ 'wppb-rf-cpt', 'wppb-epf-cpt' ].includes( postType ) ) return null;
if ( ! ( window.wppbFb && window.wppbFb.progressBar && window.wppbFb.progressBar.active ) ) return null;
return ;
};
const ProgressBarPanelInner = ( { postType } ) => {
const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
if ( ! meta ) return null;
return ;
};
const ProgressBarPanelBody = ( { meta, setMeta } ) => {
const enabled = meta.wppb_fb_progress_bar_enable === 'Yes';
const position = meta.wppb_fb_progress_bar_position || 'Top of Form';
useProgressBarReconciler( enabled, position );
return (
setMeta( { wppb_fb_progress_bar_enable: val ? 'Yes' : 'No' } ) }
help={ __( 'Show a completion progress bar on the form.', 'profile-builder' ) }
/>
{ enabled && (
<>
setMeta( { wppb_fb_progress_bar_calculation_mode: val } ) }
help={ __( 'Choose how progress is calculated.', 'profile-builder' ) }
/>
setMeta( { wppb_fb_progress_bar_display_options: val } ) }
/>
setMeta( { wppb_fb_progress_bar_position: val } ) }
help={ __( 'Where to display the progress bar.', 'profile-builder' ) }
/>
>
) }
);
};
registerPlugin( 'wppb-progress-bar-settings-plugin', {
render: ProgressBarPanel,
} );