import { useState, useEffect, useRef, useCallback, createPortal } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useDraggable } from '@dnd-kit/core'; import { getAllElements, getElement } from '../fields/FieldRegistry'; import SuggestedAmountsEditor from './SuggestedAmountsEditor'; import { openWpMedia } from '../utils/media'; import ColorPicker from './ColorPicker'; const CUSTOM_ICONS = { progress_bar: ( ), donation_form: ( ), }; /** * Right sidebar — two tabs: * "Input Fields" — 2-col grid of draggable element cards * "Input Customization" — schema-driven settings for the selected element */ export default function RightSidebar( { meta, layout, selectedElementId, selectedColumnId, dispatch, } ) { const [ activeTab, setActiveTab ] = useState( 'fields' ); useEffect( () => { if ( selectedElementId ) { setActiveTab( 'customization' ); } }, [ selectedElementId ] ); return (
{ activeTab === 'fields' ? ( ) : ( ) }
); } // ── Input Fields tab ─────────────────────────────────────────────────────────── function FieldsGrid() { const elements = getAllElements(); return (
{ elements.map( ( el ) => ( ) ) }
); } function DraggableFieldCard( { element } ) { const { attributes, listeners, setNodeRef, isDragging } = useDraggable( { id: `panel-${ element.type }`, data: { source: 'panel', elementType: element.type, defaultSettings: element.defaultSettings, }, } ); return (
{ CUSTOM_ICONS[ element.type ] ? { CUSTOM_ICONS[ element.type ] } : } { element.label }
); } // ── Input Customization tab ──────────────────────────────────────────────────── function CustomizationPanel( { meta, layout, selectedElementId, selectedColumnId, dispatch } ) { const [ innerTab, setInnerTab ] = useState( 'general' ); let selectedElement = null; if ( selectedElementId && layout?.columns ) { for ( const col of layout.columns ) { const found = col.elements.find( ( el ) => el.id === selectedElementId ); if ( found ) { selectedElement = found; break; } } } const def = selectedElement ? getElement( selectedElement.type ) : null; return (
{ /* General / Advanced tabs — always visible */ }
{ innerTab === 'general' && ( selectedElement ? ( <>
{ def?.label || __( 'Element Settings', 'better-payment' ) }
{ selectedElement.type === 'progress_bar' && (

{ __( 'Note: Progress bars will be visible only when there is a goal set for the campaign.', 'better-payment' ) }

) } { ( selectedElement.type === 'social_links' || selectedElement.type === 'social_sharing' ) && (

{ __( "Don't see a social network here that you use and would like added?", 'better-payment' ) }{ ' ' } { __( 'Let us know!', 'better-payment' ) }

) } ) : (

{ __( 'Click an element on the canvas to customize it.', 'better-payment' ) }

) ) } { innerTab === 'advanced' && ( ) }
); } // ── Theme Colors (Advanced tab) ──────────────────────────────────────────────── const TEMPLATE_BUTTON_DEFAULTS = { 'blank-1col': '#5AA152', 'blank-2col': '#5AA152', 'blank-3col': '#5AA152', 'charity-basic': '#B49A5F', 'medical-relief': '#7A8347', 'education-fund': '#8FA040', 'golf-destinations':'#B8A46A', 'disaster-relief': '#c0392b', }; const COLOR_FIELDS = [ { key: 'bpc_color_button', label: __( 'Button', 'better-payment' ), default: '#5AA152' }, ]; const PICKER_W = 270; const PICKER_H_EST = 320; // estimated picker height after size reduction function ThemeColors( { meta, dispatch } ) { const [ openKey, setOpenKey ] = useState( null ); const [ pickerPos, setPickerPos ] = useState( { top: 0, left: 0, caretLeft: 0, caretAtBottom: true } ); const swatchRefs = useRef( {} ); const pickerRef = useRef( null ); const openPickerFor = useCallback( ( key ) => { if ( openKey === key ) { setOpenKey( null ); return; } const el = swatchRefs.current[ key ]; if ( el ) { const rect = el.getBoundingClientRect(); // Horizontal: center picker over swatch, clamp to viewport. let left = rect.left + rect.width / 2 - PICKER_W / 2; left = Math.max( 8, Math.min( left, window.innerWidth - PICKER_W - 8 ) ); const caretLeft = rect.left + rect.width / 2 - left; // Vertical: prefer above the swatch, fall back to below. const spaceAbove = rect.top - 10; const caretAtBottom = spaceAbove >= PICKER_H_EST; let top = caretAtBottom ? rect.top - PICKER_H_EST - 10 : rect.bottom + 10; // Clamp so the picker never leaves the viewport. top = Math.max( 8, Math.min( top, window.innerHeight - PICKER_H_EST - 8 ) ); setPickerPos( { top, left, caretLeft, caretAtBottom } ); } setOpenKey( key ); }, [ openKey ] ); // Close picker when clicking outside both the popup and the swatches. useEffect( () => { if ( ! openKey ) return; const handler = ( e ) => { const inPicker = pickerRef.current?.contains( e.target ); const inSwatch = Object.values( swatchRefs.current ).some( el => el?.contains( e.target ) ); if ( ! inPicker && ! inSwatch ) setOpenKey( null ); }; document.addEventListener( 'mousedown', handler ); return () => document.removeEventListener( 'mousedown', handler ); }, [ openKey ] ); const update = ( key, val ) => dispatch( { type: 'UPDATE_META', payload: { [ key ]: val } } ); const activeField = COLOR_FIELDS.find( ( f ) => f.key === openKey ); return (

{ __( 'Theme Colors', 'better-payment' ) }

{ COLOR_FIELDS.map( ( { key, label, default: def } ) => { const value = meta[ key ] || def; return (
{ label }
); } ) } { openKey && activeField && createPortal(
update( openKey, hex ) } onClose={ () => setOpenKey( null ) } />
, document.body ) }
); } function ElementSettings( { element, schema, meta, dispatch } ) { const updateElement = ( patch ) => dispatch( { type: 'UPDATE_ELEMENT_SETTINGS', elementId: element.id, settings: patch } ); const updateMeta = ( patch ) => dispatch( { type: 'UPDATE_META', payload: patch } ); const s = element.settings || {}; if ( schema.length === 0 ) { return (

{ __( 'No settings for this element.', 'better-payment' ) }

); } return (
{ schema.map( ( control ) => { const isMeta = !! control.metaKey; const rawValue = isMeta ? meta?.[ control.metaKey ] : s[ control.key ]; const value = rawValue !== undefined && rawValue !== null ? rawValue : ( control.defaultValue ?? '' ); const onChange = isMeta ? ( val ) => updateMeta( { [ control.metaKey ]: val } ) : ( val ) => updateElement( { [ control.key ]: val } ); const onMultiChange = ( ! isMeta && control.type === 'image_upload' ) ? ( patch ) => updateElement( patch ) : undefined; const elementSettings = ( ! isMeta && control.type === 'image_upload' ) ? s : undefined; return ( ); } ) }
); } const ALIGN_OPTIONS = [ { value: 'left', title: 'Align left', icon: ( ), }, { value: 'center', title: 'Align center', icon: ( ), }, { value: 'right', title: 'Align right', icon: ( ), }, ]; // ── SVG icons for the rich-text toolbar ────────────────────────────────────── const TB = { bold: , italic: , under: , link: , ol: , ul: , clear: , }; const BLOCKS = [ { value: 'p', label: 'Normal' }, { value: 'h2', label: 'Heading 2' }, { value: 'h3', label: 'Heading 3' }, { value: 'h4', label: 'Heading 4' }, ]; const QUERY_CMDS = [ 'bold', 'italic', 'underline', 'insertOrderedList', 'insertUnorderedList' ]; function RichTextControl( { value, onChange } ) { const editorRef = useRef( null ); const containerRef = useRef( null ); const focusedRef = useRef( false ); const lastValueRef = useRef( value ); const savedRangeRef = useRef( null ); const linkInputRef = useRef( null ); const [ activeFormats, setActiveFormats ] = useState( {} ); const [ blockValue, setBlockValue ] = useState( 'p' ); const [ linkPopover, setLinkPopover ] = useState( null ); // linkPopover: null | { mode: 'insert'|'view', href: string, anchorEl: Node|null, top: number, left: number } // Seed editor on mount useEffect( () => { if ( editorRef.current ) { editorRef.current.innerHTML = value || ''; lastValueRef.current = value || ''; } }, [] ); // eslint-disable-line react-hooks/exhaustive-deps // Sync external value changes when not focused (different element selected) useEffect( () => { if ( editorRef.current && ! focusedRef.current && value !== lastValueRef.current ) { editorRef.current.innerHTML = value || ''; lastValueRef.current = value || ''; } }, [ value ] ); // Auto-focus link input when popover opens in insert mode useEffect( () => { if ( linkPopover?.mode === 'insert' && linkInputRef.current ) { linkInputRef.current.focus(); linkInputRef.current.select(); } }, [ linkPopover ] ); // Close popover on outside click useEffect( () => { if ( ! linkPopover ) return; const onDown = ( e ) => { if ( ! containerRef.current?.contains( e.target ) ) { setLinkPopover( null ); } }; document.addEventListener( 'mousedown', onDown ); return () => document.removeEventListener( 'mousedown', onDown ); }, [ linkPopover ] ); const updateActiveState = useCallback( () => { const formats = {}; QUERY_CMDS.forEach( ( c ) => { try { formats[ c ] = document.queryCommandState( c ); } catch ( e ) { formats[ c ] = false; } } ); // Extra check for list context via DOM traversal (more reliable cross-browser) const sel = window.getSelection(); if ( sel && sel.rangeCount ) { let node = sel.getRangeAt( 0 ).startContainer; while ( node && node !== editorRef.current ) { const tag = node.nodeName?.toLowerCase(); if ( tag === 'ul' ) { formats.insertUnorderedList = true; break; } if ( tag === 'ol' ) { formats.insertOrderedList = true; break; } if ( tag === 'li' ) { const parent = node.parentNode?.nodeName?.toLowerCase(); if ( parent === 'ul' ) formats.insertUnorderedList = true; if ( parent === 'ol' ) formats.insertOrderedList = true; break; } node = node.parentNode; } } setActiveFormats( formats ); // Detect current block tag if ( sel && sel.rangeCount ) { let node = sel.getRangeAt( 0 ).startContainer; while ( node && node !== editorRef.current ) { const tag = node.nodeName?.toLowerCase(); if ( [ 'p', 'h2', 'h3', 'h4' ].includes( tag ) ) { setBlockValue( tag ); return; } node = node.parentNode; } } setBlockValue( 'p' ); }, [] ); const cmd = useCallback( ( command, arg = null ) => { editorRef.current?.focus(); document.execCommand( command, false, arg ); updateActiveState(); }, [ updateActiveState ] ); const handleInput = useCallback( () => { const html = editorRef.current?.innerHTML || ''; lastValueRef.current = html; onChange( html ); }, [ onChange ] ); // Save the current selection and compute popover position relative to container const openLinkPopover = useCallback( ( e ) => { e.preventDefault(); const sel = window.getSelection(); if ( ! sel || ! sel.rangeCount ) return; const range = sel.getRangeAt( 0 ); savedRangeRef.current = range.cloneRange(); // Find if cursor/selection is inside an anchor let anchor = null; let node = range.startContainer; while ( node && node !== editorRef.current ) { if ( node.nodeName === 'A' ) { anchor = node; break; } node = node.parentNode; } // Position below the selection, spanning the full container width (left/right set via CSS) const containerRect = containerRef.current?.getBoundingClientRect() ?? { top: 0 }; const selRect = range.getBoundingClientRect(); const top = selRect.bottom - containerRect.top + 4; if ( anchor ) { setLinkPopover( { mode: 'view', href: anchor.href, anchorEl: anchor, top } ); } else { setLinkPopover( { mode: 'insert', href: 'https://', anchorEl: null, top } ); } }, [] ); // Restore saved selection, then apply execCommand const restoreAndExec = useCallback( ( command, arg = null ) => { editorRef.current?.focus(); if ( savedRangeRef.current ) { const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange( savedRangeRef.current ); } document.execCommand( command, false, arg ); handleInput(); updateActiveState(); }, [ handleInput, updateActiveState ] ); const handleLinkSave = useCallback( ( href ) => { setLinkPopover( null ); if ( href && href !== 'https://' ) { restoreAndExec( 'createLink', href ); } }, [ restoreAndExec ] ); const handleLinkRemove = useCallback( () => { setLinkPopover( null ); // Select the full anchor node before unlinking const anchor = linkPopover?.anchorEl; if ( anchor && editorRef.current?.contains( anchor ) ) { editorRef.current.focus(); const range = document.createRange(); range.selectNode( anchor ); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange( range ); document.execCommand( 'unlink', false, null ); handleInput(); } }, [ linkPopover, handleInput ] ); const handleLinkEdit = useCallback( () => { setLinkPopover( ( prev ) => ( { ...prev, mode: 'insert' } ) ); }, [] ); const handleBlockChange = useCallback( ( e ) => { const tag = e.target.value; editorRef.current?.focus(); document.execCommand( 'formatBlock', false, tag ); setBlockValue( tag ); }, [] ); const BTNS = [ { id: 'bold', icon: TB.bold, title: 'Bold (Ctrl+B)' }, { id: 'italic', icon: TB.italic,title: 'Italic (Ctrl+I)' }, { id: 'underline', icon: TB.under, title: 'Underline (Ctrl+U)' }, { id: 'insertOrderedList', icon: TB.ol, title: 'Ordered list' }, { id: 'insertUnorderedList', icon: TB.ul, title: 'Unordered list' }, { id: 'removeFormat', icon: TB.clear, title: 'Clear formatting' }, { id: 'link', icon: TB.link, title: 'Insert link', isLink: true }, ]; return (
{ /* Row 1: block style dropdown */ }
{ /* Row 2: format buttons */ }
{ BTNS.map( ( btn ) => ( ) ) }
{ /* Inline link popover */ } { linkPopover && (
{ if ( e.key === 'Escape' ) setLinkPopover( null ); } } > { linkPopover.mode === 'insert' ? ( <> { __( 'Enter link:', 'better-payment' ) } { if ( e.key === 'Enter' ) { e.preventDefault(); handleLinkSave( e.target.value ); } } } /> ) : ( <> { linkPopover.href.length > 40 ? linkPopover.href.slice( 0, 40 ) + '…' : linkPopover.href } | | ) }
) }
{ focusedRef.current = true; } } onBlur={ () => { focusedRef.current = false; handleInput(); } } onInput={ handleInput } onKeyUp={ updateActiveState } onMouseUp={ updateActiveState } />
); } function ImageUploadControl( { value, onMultiChange, elementSettings } ) { const openMedia = useCallback( () => { openWpMedia( ( { url, id, sizes } ) => onMultiChange( { src: url, src_id: id, src_sizes: sizes } ) ); }, [ onMultiChange ] ); const srcSizes = elementSettings?.src_sizes || {}; const size = elementSettings?.size || 'full'; const displaySrc = ( size !== 'full' && srcSizes[ size ]?.url ) ? srcSizes[ size ].url : ( value || '' ); const hasImage = !! displaySrc; return (
{ /* Elementor-style preview area */ }
{ hasImage ? ( <> ) : (
) } { /* Choose Image bar */ }
e.stopPropagation() }>
{ /* URL input + Clear */ } onMultiChange( { src: e.target.value, src_id: 0, src_sizes: {} } ) } /> { hasImage && ( ) }
); } function SettingsControl( { control, value, onChange, onMultiChange, elementSettings } ) { const { key, label, type, placeholder, min, max, rows, options, info, unit, step } = control; if ( type === 'section_label' ) { return (
{ label }
); } if ( type === 'note' ) { return

{ label }

; } return (
{ type !== 'toggle' && type !== 'switch' && type !== 'image_upload' && type !== 'rich_text' && ( ) } { type === 'image_upload' && ( <> ) } { type === 'rich_text' && ( <> ) } { type === 'range' && (
onChange( parseInt( e.target.value, 10 ) ) } /> { value ?? ( control.defaultValue ?? 100 ) }{ unit || '%' }
) } { type === 'align' && (
{ ALIGN_OPTIONS.map( ( opt ) => ( ) ) }
) } { type === 'text' && ( onChange( e.target.value ) } /> ) } { type === 'url' && ( onChange( e.target.value ) } /> ) } { type === 'number' && ( onChange( parseInt( e.target.value, 10 ) ) } /> ) } { type === 'color' && (
onChange( e.target.value ) } /> { value || '#6b63f6' }
) } { type === 'toggle' && ( ) } { type === 'switch' && ( ) } { type === 'select' && ( ) } { type === 'textarea' && (