| 1 |
import { useState, useEffect, useRef, useCallback, createPortal } from '@wordpress/element'; |
| 2 |
import { __ } from '@wordpress/i18n'; |
| 3 |
import { useDraggable } from '@dnd-kit/core'; |
| 4 |
import { getAllElements, getElement } from '../fields/FieldRegistry'; |
| 5 |
import SuggestedAmountsEditor from './SuggestedAmountsEditor'; |
| 6 |
import { openWpMedia } from '../utils/media'; |
| 7 |
import ColorPicker from './ColorPicker'; |
| 8 |
|
| 9 |
const CUSTOM_ICONS = { |
| 10 |
progress_bar: ( |
| 11 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 12 |
<rect x="2" y="10" width="20" height="4" rx="2" fill="currentColor" fillOpacity="0.22"/> |
| 13 |
<rect x="2" y="10" width="12" height="4" rx="2" fill="currentColor"/> |
| 14 |
<circle cx="14" cy="12" r="3" fill="currentColor"/> |
| 15 |
</svg> |
| 16 |
), |
| 17 |
donation_form: ( |
| 18 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 19 |
<path d="M12 9c-.5-1.3-1.8-2.2-3.2-2.2C6.9 6.8 5.4 8.3 5.4 10.1c0 2.3 2.3 4.4 6.6 7.1 4.3-2.7 6.6-4.8 6.6-7.1 0-1.8-1.5-3.3-3.4-3.3C13.8 6.8 12.5 7.7 12 9z" fill="currentColor"/> |
| 20 |
<rect x="6" y="18.5" width="12" height="3" rx="1.5" stroke="currentColor" strokeWidth="1.4" fill="none"/> |
| 21 |
<line x1="9.5" y1="18.5" x2="9.5" y2="17" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/> |
| 22 |
<line x1="12" y1="18.5" x2="12" y2="16.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/> |
| 23 |
<line x1="14.5" y1="18.5" x2="14.5" y2="17" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/> |
| 24 |
</svg> |
| 25 |
), |
| 26 |
}; |
| 27 |
|
| 28 |
/** |
| 29 |
* Right sidebar — two tabs: |
| 30 |
* "Input Fields" — 2-col grid of draggable element cards |
| 31 |
* "Input Customization" — schema-driven settings for the selected element |
| 32 |
*/ |
| 33 |
export default function RightSidebar( { |
| 34 |
meta, |
| 35 |
layout, |
| 36 |
selectedElementId, |
| 37 |
selectedColumnId, |
| 38 |
dispatch, |
| 39 |
} ) { |
| 40 |
const [ activeTab, setActiveTab ] = useState( 'fields' ); |
| 41 |
|
| 42 |
useEffect( () => { |
| 43 |
if ( selectedElementId ) { |
| 44 |
setActiveTab( 'customization' ); |
| 45 |
} |
| 46 |
}, [ selectedElementId ] ); |
| 47 |
|
| 48 |
return ( |
| 49 |
<div className="bp-cb-right-sidebar"> |
| 50 |
<div className="bp-cb-right-sidebar__tabs"> |
| 51 |
<button |
| 52 |
className={ `bp-cb-sidebar-tab${ activeTab === 'fields' ? ' is-active' : '' }` } |
| 53 |
onClick={ () => setActiveTab( 'fields' ) } |
| 54 |
> |
| 55 |
{ __( 'Elements', 'better-payment' ) } |
| 56 |
</button> |
| 57 |
<button |
| 58 |
className={ `bp-cb-sidebar-tab${ activeTab === 'customization' ? ' is-active' : '' }` } |
| 59 |
onClick={ () => setActiveTab( 'customization' ) } |
| 60 |
> |
| 61 |
{ __( 'Customize Element', 'better-payment' ) } |
| 62 |
</button> |
| 63 |
</div> |
| 64 |
|
| 65 |
<div className="bp-cb-right-sidebar__body"> |
| 66 |
{ activeTab === 'fields' ? ( |
| 67 |
<FieldsGrid /> |
| 68 |
) : ( |
| 69 |
<CustomizationPanel |
| 70 |
meta={ meta } |
| 71 |
layout={ layout } |
| 72 |
selectedElementId={ selectedElementId } |
| 73 |
selectedColumnId={ selectedColumnId } |
| 74 |
dispatch={ dispatch } |
| 75 |
/> |
| 76 |
) } |
| 77 |
</div> |
| 78 |
</div> |
| 79 |
); |
| 80 |
} |
| 81 |
|
| 82 |
// ── Input Fields tab ─────────────────────────────────────────────────────────── |
| 83 |
|
| 84 |
function FieldsGrid() { |
| 85 |
const elements = getAllElements(); |
| 86 |
|
| 87 |
return ( |
| 88 |
<div className="bp-cb-fields-grid"> |
| 89 |
{ elements.map( ( el ) => ( |
| 90 |
<DraggableFieldCard key={ el.type } element={ el } /> |
| 91 |
) ) } |
| 92 |
</div> |
| 93 |
); |
| 94 |
} |
| 95 |
|
| 96 |
function DraggableFieldCard( { element } ) { |
| 97 |
const { attributes, listeners, setNodeRef, isDragging } = useDraggable( { |
| 98 |
id: `panel-${ element.type }`, |
| 99 |
data: { |
| 100 |
source: 'panel', |
| 101 |
elementType: element.type, |
| 102 |
defaultSettings: element.defaultSettings, |
| 103 |
}, |
| 104 |
} ); |
| 105 |
|
| 106 |
return ( |
| 107 |
<div |
| 108 |
ref={ setNodeRef } |
| 109 |
className={ `bp-cb-field-card${ isDragging ? ' is-dragging' : '' }` } |
| 110 |
title={ element.label } |
| 111 |
{ ...listeners } |
| 112 |
{ ...attributes } |
| 113 |
> |
| 114 |
{ CUSTOM_ICONS[ element.type ] |
| 115 |
? <span className="bp-cb-field-card__svg-icon">{ CUSTOM_ICONS[ element.type ] }</span> |
| 116 |
: <span className={ `dashicons dashicons-${ element.icon }` }></span> |
| 117 |
} |
| 118 |
<span className="bp-cb-field-card__label">{ element.label }</span> |
| 119 |
</div> |
| 120 |
); |
| 121 |
} |
| 122 |
|
| 123 |
// ── Input Customization tab ──────────────────────────────────────────────────── |
| 124 |
|
| 125 |
|
| 126 |
function CustomizationPanel( { meta, layout, selectedElementId, selectedColumnId, dispatch } ) { |
| 127 |
const [ innerTab, setInnerTab ] = useState( 'general' ); |
| 128 |
|
| 129 |
let selectedElement = null; |
| 130 |
if ( selectedElementId && layout?.columns ) { |
| 131 |
for ( const col of layout.columns ) { |
| 132 |
const found = col.elements.find( ( el ) => el.id === selectedElementId ); |
| 133 |
if ( found ) { selectedElement = found; break; } |
| 134 |
} |
| 135 |
} |
| 136 |
|
| 137 |
const def = selectedElement ? getElement( selectedElement.type ) : null; |
| 138 |
|
| 139 |
return ( |
| 140 |
<div className="bp-cb-element-settings"> |
| 141 |
|
| 142 |
{ /* General / Advanced tabs — always visible */ } |
| 143 |
<div className="bp-cb-inner-tabs"> |
| 144 |
<button |
| 145 |
className={ `bp-cb-inner-tab${ innerTab === 'general' ? ' is-active' : '' }` } |
| 146 |
onClick={ () => setInnerTab( 'general' ) } |
| 147 |
> |
| 148 |
{ __( 'General', 'better-payment' ) } |
| 149 |
</button> |
| 150 |
<button |
| 151 |
className={ `bp-cb-inner-tab${ innerTab === 'advanced' ? ' is-active' : '' }` } |
| 152 |
onClick={ () => setInnerTab( 'advanced' ) } |
| 153 |
> |
| 154 |
{ __( 'Advanced', 'better-payment' ) } |
| 155 |
</button> |
| 156 |
</div> |
| 157 |
|
| 158 |
{ innerTab === 'general' && ( |
| 159 |
selectedElement ? ( |
| 160 |
<> |
| 161 |
<div className="bp-cb-element-settings__header"> |
| 162 |
<span className={ `dashicons dashicons-${ def?.icon || 'block-default' }` } /> |
| 163 |
<span className="bp-cb-element-settings__title"> |
| 164 |
{ def?.label || __( 'Element Settings', 'better-payment' ) } |
| 165 |
</span> |
| 166 |
</div> |
| 167 |
|
| 168 |
{ selectedElement.type === 'progress_bar' && ( |
| 169 |
<p className="bp-cb-widget-note"> |
| 170 |
{ __( 'Note: Progress bars will be visible only when there is a goal set for the campaign.', 'better-payment' ) } |
| 171 |
</p> |
| 172 |
) } |
| 173 |
|
| 174 |
{ ( selectedElement.type === 'social_links' || selectedElement.type === 'social_sharing' ) && ( |
| 175 |
<p className="bp-cb-social-info"> |
| 176 |
{ __( "Don't see a social network here that you use and would like added?", 'better-payment' ) }{ ' ' } |
| 177 |
<a href="https://wpdeveloper.com/support" target="_blank" rel="noreferrer"> |
| 178 |
{ __( 'Let us know!', 'better-payment' ) } |
| 179 |
</a> |
| 180 |
</p> |
| 181 |
) } |
| 182 |
|
| 183 |
<ElementSettings |
| 184 |
element={ selectedElement } |
| 185 |
schema={ def?.settingsSchema || [] } |
| 186 |
meta={ meta } |
| 187 |
dispatch={ dispatch } |
| 188 |
/> |
| 189 |
</> |
| 190 |
) : ( |
| 191 |
<div className="bp-cb-customization-empty"> |
| 192 |
<svg width="40" height="40" viewBox="0 0 40 40" fill="none"> |
| 193 |
<circle cx="20" cy="20" r="18" stroke="#ddd" strokeWidth="2"/> |
| 194 |
<path d="M14 20h12M20 14v12" stroke="#ccc" strokeWidth="2" strokeLinecap="round"/> |
| 195 |
</svg> |
| 196 |
<p>{ __( 'Click an element on the canvas to customize it.', 'better-payment' ) }</p> |
| 197 |
</div> |
| 198 |
) |
| 199 |
) } |
| 200 |
|
| 201 |
{ innerTab === 'advanced' && ( |
| 202 |
<ThemeColors meta={ meta } dispatch={ dispatch } /> |
| 203 |
) } |
| 204 |
</div> |
| 205 |
); |
| 206 |
} |
| 207 |
|
| 208 |
// ── Theme Colors (Advanced tab) ──────────────────────────────────────────────── |
| 209 |
|
| 210 |
const TEMPLATE_BUTTON_DEFAULTS = { |
| 211 |
'blank-1col': '#5AA152', |
| 212 |
'blank-2col': '#5AA152', |
| 213 |
'blank-3col': '#5AA152', |
| 214 |
'charity-basic': '#B49A5F', |
| 215 |
'medical-relief': '#7A8347', |
| 216 |
'education-fund': '#8FA040', |
| 217 |
'golf-destinations':'#B8A46A', |
| 218 |
'disaster-relief': '#c0392b', |
| 219 |
}; |
| 220 |
|
| 221 |
const COLOR_FIELDS = [ |
| 222 |
{ key: 'bpc_color_button', label: __( 'Button', 'better-payment' ), default: '#5AA152' }, |
| 223 |
]; |
| 224 |
|
| 225 |
const PICKER_W = 270; |
| 226 |
const PICKER_H_EST = 320; // estimated picker height after size reduction |
| 227 |
|
| 228 |
function ThemeColors( { meta, dispatch } ) { |
| 229 |
const [ openKey, setOpenKey ] = useState( null ); |
| 230 |
const [ pickerPos, setPickerPos ] = useState( { top: 0, left: 0, caretLeft: 0, caretAtBottom: true } ); |
| 231 |
const swatchRefs = useRef( {} ); |
| 232 |
const pickerRef = useRef( null ); |
| 233 |
|
| 234 |
const openPickerFor = useCallback( ( key ) => { |
| 235 |
if ( openKey === key ) { setOpenKey( null ); return; } |
| 236 |
|
| 237 |
const el = swatchRefs.current[ key ]; |
| 238 |
if ( el ) { |
| 239 |
const rect = el.getBoundingClientRect(); |
| 240 |
|
| 241 |
// Horizontal: center picker over swatch, clamp to viewport. |
| 242 |
let left = rect.left + rect.width / 2 - PICKER_W / 2; |
| 243 |
left = Math.max( 8, Math.min( left, window.innerWidth - PICKER_W - 8 ) ); |
| 244 |
const caretLeft = rect.left + rect.width / 2 - left; |
| 245 |
|
| 246 |
// Vertical: prefer above the swatch, fall back to below. |
| 247 |
const spaceAbove = rect.top - 10; |
| 248 |
const caretAtBottom = spaceAbove >= PICKER_H_EST; |
| 249 |
let top = caretAtBottom |
| 250 |
? rect.top - PICKER_H_EST - 10 |
| 251 |
: rect.bottom + 10; |
| 252 |
|
| 253 |
// Clamp so the picker never leaves the viewport. |
| 254 |
top = Math.max( 8, Math.min( top, window.innerHeight - PICKER_H_EST - 8 ) ); |
| 255 |
|
| 256 |
setPickerPos( { top, left, caretLeft, caretAtBottom } ); |
| 257 |
} |
| 258 |
setOpenKey( key ); |
| 259 |
}, [ openKey ] ); |
| 260 |
|
| 261 |
// Close picker when clicking outside both the popup and the swatches. |
| 262 |
useEffect( () => { |
| 263 |
if ( ! openKey ) return; |
| 264 |
const handler = ( e ) => { |
| 265 |
const inPicker = pickerRef.current?.contains( e.target ); |
| 266 |
const inSwatch = Object.values( swatchRefs.current ).some( el => el?.contains( e.target ) ); |
| 267 |
if ( ! inPicker && ! inSwatch ) setOpenKey( null ); |
| 268 |
}; |
| 269 |
document.addEventListener( 'mousedown', handler ); |
| 270 |
return () => document.removeEventListener( 'mousedown', handler ); |
| 271 |
}, [ openKey ] ); |
| 272 |
|
| 273 |
const update = ( key, val ) => |
| 274 |
dispatch( { type: 'UPDATE_META', payload: { [ key ]: val } } ); |
| 275 |
|
| 276 |
const activeField = COLOR_FIELDS.find( ( f ) => f.key === openKey ); |
| 277 |
|
| 278 |
return ( |
| 279 |
<div className="bp-theme-colors"> |
| 280 |
<p className="bp-theme-colors__title"> |
| 281 |
{ __( 'Theme Colors', 'better-payment' ) } |
| 282 |
</p> |
| 283 |
|
| 284 |
{ COLOR_FIELDS.map( ( { key, label, default: def } ) => { |
| 285 |
const value = meta[ key ] || def; |
| 286 |
return ( |
| 287 |
<div key={ key } className="bp-theme-color-row"> |
| 288 |
<span className="bp-theme-color-row__label">{ label }</span> |
| 289 |
<div className="bp-theme-color-row__control"> |
| 290 |
<button |
| 291 |
ref={ ( el ) => { swatchRefs.current[ key ] = el; } } |
| 292 |
type="button" |
| 293 |
className={ `bp-theme-color-row__swatch${ openKey === key ? ' is-active' : '' }` } |
| 294 |
style={ { background: value } } |
| 295 |
onClick={ () => openPickerFor( key ) } |
| 296 |
/> |
| 297 |
<input |
| 298 |
type="text" |
| 299 |
className="bp-theme-color-row__input" |
| 300 |
value={ value } |
| 301 |
readOnly |
| 302 |
onClick={ () => openPickerFor( key ) } |
| 303 |
/> |
| 304 |
</div> |
| 305 |
</div> |
| 306 |
); |
| 307 |
} ) } |
| 308 |
|
| 309 |
{ openKey && activeField && createPortal( |
| 310 |
<div |
| 311 |
ref={ pickerRef } |
| 312 |
className="bp-color-picker-popup" |
| 313 |
style={ { |
| 314 |
position: 'fixed', |
| 315 |
top: pickerPos.top, |
| 316 |
left: pickerPos.left, |
| 317 |
width: PICKER_W, |
| 318 |
zIndex: 999999, |
| 319 |
} } |
| 320 |
> |
| 321 |
<ColorPicker |
| 322 |
value={ meta[ openKey ] || activeField.default } |
| 323 |
defaultValue={ |
| 324 |
TEMPLATE_BUTTON_DEFAULTS[ meta.bpc_template_key ] || activeField.default |
| 325 |
} |
| 326 |
onChange={ ( hex ) => update( openKey, hex ) } |
| 327 |
onClose={ () => setOpenKey( null ) } |
| 328 |
/> |
| 329 |
<div |
| 330 |
className={ `bp-color-picker-popup__caret${ pickerPos.caretAtBottom ? '' : ' is-top' }` } |
| 331 |
style={ { left: pickerPos.caretLeft } } |
| 332 |
/> |
| 333 |
</div>, |
| 334 |
document.body |
| 335 |
) } |
| 336 |
</div> |
| 337 |
); |
| 338 |
} |
| 339 |
|
| 340 |
function ElementSettings( { element, schema, meta, dispatch } ) { |
| 341 |
const updateElement = ( patch ) => |
| 342 |
dispatch( { type: 'UPDATE_ELEMENT_SETTINGS', elementId: element.id, settings: patch } ); |
| 343 |
const updateMeta = ( patch ) => |
| 344 |
dispatch( { type: 'UPDATE_META', payload: patch } ); |
| 345 |
const s = element.settings || {}; |
| 346 |
|
| 347 |
if ( schema.length === 0 ) { |
| 348 |
return ( |
| 349 |
<p className="bp-cb-customization-hint"> |
| 350 |
{ __( 'No settings for this element.', 'better-payment' ) } |
| 351 |
</p> |
| 352 |
); |
| 353 |
} |
| 354 |
|
| 355 |
return ( |
| 356 |
<div className="bp-cb-settings-form"> |
| 357 |
{ schema.map( ( control ) => { |
| 358 |
const isMeta = !! control.metaKey; |
| 359 |
const rawValue = isMeta ? meta?.[ control.metaKey ] : s[ control.key ]; |
| 360 |
const value = rawValue !== undefined && rawValue !== null ? rawValue : ( control.defaultValue ?? '' ); |
| 361 |
const onChange = isMeta |
| 362 |
? ( val ) => updateMeta( { [ control.metaKey ]: val } ) |
| 363 |
: ( val ) => updateElement( { [ control.key ]: val } ); |
| 364 |
const onMultiChange = ( ! isMeta && control.type === 'image_upload' ) |
| 365 |
? ( patch ) => updateElement( patch ) |
| 366 |
: undefined; |
| 367 |
const elementSettings = ( ! isMeta && control.type === 'image_upload' ) ? s : undefined; |
| 368 |
return ( |
| 369 |
<SettingsControl |
| 370 |
key={ control.key } |
| 371 |
control={ control } |
| 372 |
value={ value } |
| 373 |
onChange={ onChange } |
| 374 |
onMultiChange={ onMultiChange } |
| 375 |
elementSettings={ elementSettings } |
| 376 |
/> |
| 377 |
); |
| 378 |
} ) } |
| 379 |
</div> |
| 380 |
); |
| 381 |
} |
| 382 |
|
| 383 |
const ALIGN_OPTIONS = [ |
| 384 |
{ |
| 385 |
value: 'left', |
| 386 |
title: 'Align left', |
| 387 |
icon: ( |
| 388 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"> |
| 389 |
<rect x="2" y="4" width="16" height="2" rx="1" fill="currentColor"/> |
| 390 |
<rect x="2" y="9" width="11" height="2" rx="1" fill="currentColor"/> |
| 391 |
<rect x="2" y="14" width="14" height="2" rx="1" fill="currentColor"/> |
| 392 |
</svg> |
| 393 |
), |
| 394 |
}, |
| 395 |
{ |
| 396 |
value: 'center', |
| 397 |
title: 'Align center', |
| 398 |
icon: ( |
| 399 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"> |
| 400 |
<rect x="2" y="4" width="16" height="2" rx="1" fill="currentColor"/> |
| 401 |
<rect x="4.5" y="9" width="11" height="2" rx="1" fill="currentColor"/> |
| 402 |
<rect x="3" y="14" width="14" height="2" rx="1" fill="currentColor"/> |
| 403 |
</svg> |
| 404 |
), |
| 405 |
}, |
| 406 |
{ |
| 407 |
value: 'right', |
| 408 |
title: 'Align right', |
| 409 |
icon: ( |
| 410 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"> |
| 411 |
<rect x="2" y="4" width="16" height="2" rx="1" fill="currentColor"/> |
| 412 |
<rect x="7" y="9" width="11" height="2" rx="1" fill="currentColor"/> |
| 413 |
<rect x="4" y="14" width="14" height="2" rx="1" fill="currentColor"/> |
| 414 |
</svg> |
| 415 |
), |
| 416 |
}, |
| 417 |
]; |
| 418 |
|
| 419 |
// ── SVG icons for the rich-text toolbar ────────────────────────────────────── |
| 420 |
const TB = { |
| 421 |
bold: <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h8a4 4 0 010 8H6zm0 8h9a4 4 0 010 8H6z" stroke="currentColor" strokeWidth="2" fill="none" strokeLinejoin="round"/></svg>, |
| 422 |
italic: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>, |
| 423 |
under: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M6 4v6a6 6 0 0012 0V4"/><line x1="4" y1="20" x2="20" y2="20"/></svg>, |
| 424 |
link: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>, |
| 425 |
ol: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>, |
| 426 |
ul: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor"/><circle cx="4" cy="12" r="1" fill="currentColor"/><circle cx="4" cy="18" r="1" fill="currentColor"/></svg>, |
| 427 |
clear: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M17 7l-10 10"/><path d="M7 7h10v10"/></svg>, |
| 428 |
}; |
| 429 |
|
| 430 |
const BLOCKS = [ |
| 431 |
{ value: 'p', label: 'Normal' }, |
| 432 |
{ value: 'h2', label: 'Heading 2' }, |
| 433 |
{ value: 'h3', label: 'Heading 3' }, |
| 434 |
{ value: 'h4', label: 'Heading 4' }, |
| 435 |
]; |
| 436 |
|
| 437 |
const QUERY_CMDS = [ 'bold', 'italic', 'underline', 'insertOrderedList', 'insertUnorderedList' ]; |
| 438 |
|
| 439 |
function RichTextControl( { value, onChange } ) { |
| 440 |
const editorRef = useRef( null ); |
| 441 |
const containerRef = useRef( null ); |
| 442 |
const focusedRef = useRef( false ); |
| 443 |
const lastValueRef = useRef( value ); |
| 444 |
const savedRangeRef = useRef( null ); |
| 445 |
const linkInputRef = useRef( null ); |
| 446 |
const [ activeFormats, setActiveFormats ] = useState( {} ); |
| 447 |
const [ blockValue, setBlockValue ] = useState( 'p' ); |
| 448 |
const [ linkPopover, setLinkPopover ] = useState( null ); |
| 449 |
// linkPopover: null | { mode: 'insert'|'view', href: string, anchorEl: Node|null, top: number, left: number } |
| 450 |
|
| 451 |
// Seed editor on mount |
| 452 |
useEffect( () => { |
| 453 |
if ( editorRef.current ) { |
| 454 |
editorRef.current.innerHTML = value || ''; |
| 455 |
lastValueRef.current = value || ''; |
| 456 |
} |
| 457 |
}, [] ); // eslint-disable-line react-hooks/exhaustive-deps |
| 458 |
|
| 459 |
// Sync external value changes when not focused (different element selected) |
| 460 |
useEffect( () => { |
| 461 |
if ( editorRef.current && ! focusedRef.current && value !== lastValueRef.current ) { |
| 462 |
editorRef.current.innerHTML = value || ''; |
| 463 |
lastValueRef.current = value || ''; |
| 464 |
} |
| 465 |
}, [ value ] ); |
| 466 |
|
| 467 |
// Auto-focus link input when popover opens in insert mode |
| 468 |
useEffect( () => { |
| 469 |
if ( linkPopover?.mode === 'insert' && linkInputRef.current ) { |
| 470 |
linkInputRef.current.focus(); |
| 471 |
linkInputRef.current.select(); |
| 472 |
} |
| 473 |
}, [ linkPopover ] ); |
| 474 |
|
| 475 |
// Close popover on outside click |
| 476 |
useEffect( () => { |
| 477 |
if ( ! linkPopover ) return; |
| 478 |
const onDown = ( e ) => { |
| 479 |
if ( ! containerRef.current?.contains( e.target ) ) { |
| 480 |
setLinkPopover( null ); |
| 481 |
} |
| 482 |
}; |
| 483 |
document.addEventListener( 'mousedown', onDown ); |
| 484 |
return () => document.removeEventListener( 'mousedown', onDown ); |
| 485 |
}, [ linkPopover ] ); |
| 486 |
|
| 487 |
const updateActiveState = useCallback( () => { |
| 488 |
const formats = {}; |
| 489 |
QUERY_CMDS.forEach( ( c ) => { |
| 490 |
try { formats[ c ] = document.queryCommandState( c ); } catch ( e ) { formats[ c ] = false; } |
| 491 |
} ); |
| 492 |
|
| 493 |
// Extra check for list context via DOM traversal (more reliable cross-browser) |
| 494 |
const sel = window.getSelection(); |
| 495 |
if ( sel && sel.rangeCount ) { |
| 496 |
let node = sel.getRangeAt( 0 ).startContainer; |
| 497 |
while ( node && node !== editorRef.current ) { |
| 498 |
const tag = node.nodeName?.toLowerCase(); |
| 499 |
if ( tag === 'ul' ) { formats.insertUnorderedList = true; break; } |
| 500 |
if ( tag === 'ol' ) { formats.insertOrderedList = true; break; } |
| 501 |
if ( tag === 'li' ) { |
| 502 |
const parent = node.parentNode?.nodeName?.toLowerCase(); |
| 503 |
if ( parent === 'ul' ) formats.insertUnorderedList = true; |
| 504 |
if ( parent === 'ol' ) formats.insertOrderedList = true; |
| 505 |
break; |
| 506 |
} |
| 507 |
node = node.parentNode; |
| 508 |
} |
| 509 |
} |
| 510 |
|
| 511 |
setActiveFormats( formats ); |
| 512 |
|
| 513 |
// Detect current block tag |
| 514 |
if ( sel && sel.rangeCount ) { |
| 515 |
let node = sel.getRangeAt( 0 ).startContainer; |
| 516 |
while ( node && node !== editorRef.current ) { |
| 517 |
const tag = node.nodeName?.toLowerCase(); |
| 518 |
if ( [ 'p', 'h2', 'h3', 'h4' ].includes( tag ) ) { |
| 519 |
setBlockValue( tag ); |
| 520 |
return; |
| 521 |
} |
| 522 |
node = node.parentNode; |
| 523 |
} |
| 524 |
} |
| 525 |
setBlockValue( 'p' ); |
| 526 |
}, [] ); |
| 527 |
|
| 528 |
const cmd = useCallback( ( command, arg = null ) => { |
| 529 |
editorRef.current?.focus(); |
| 530 |
document.execCommand( command, false, arg ); |
| 531 |
updateActiveState(); |
| 532 |
}, [ updateActiveState ] ); |
| 533 |
|
| 534 |
const handleInput = useCallback( () => { |
| 535 |
const html = editorRef.current?.innerHTML || ''; |
| 536 |
lastValueRef.current = html; |
| 537 |
onChange( html ); |
| 538 |
}, [ onChange ] ); |
| 539 |
|
| 540 |
// Save the current selection and compute popover position relative to container |
| 541 |
const openLinkPopover = useCallback( ( e ) => { |
| 542 |
e.preventDefault(); |
| 543 |
const sel = window.getSelection(); |
| 544 |
if ( ! sel || ! sel.rangeCount ) return; |
| 545 |
|
| 546 |
const range = sel.getRangeAt( 0 ); |
| 547 |
savedRangeRef.current = range.cloneRange(); |
| 548 |
|
| 549 |
// Find if cursor/selection is inside an anchor |
| 550 |
let anchor = null; |
| 551 |
let node = range.startContainer; |
| 552 |
while ( node && node !== editorRef.current ) { |
| 553 |
if ( node.nodeName === 'A' ) { anchor = node; break; } |
| 554 |
node = node.parentNode; |
| 555 |
} |
| 556 |
|
| 557 |
// Position below the selection, spanning the full container width (left/right set via CSS) |
| 558 |
const containerRect = containerRef.current?.getBoundingClientRect() ?? { top: 0 }; |
| 559 |
const selRect = range.getBoundingClientRect(); |
| 560 |
const top = selRect.bottom - containerRect.top + 4; |
| 561 |
|
| 562 |
if ( anchor ) { |
| 563 |
setLinkPopover( { mode: 'view', href: anchor.href, anchorEl: anchor, top } ); |
| 564 |
} else { |
| 565 |
setLinkPopover( { mode: 'insert', href: 'https://', anchorEl: null, top } ); |
| 566 |
} |
| 567 |
}, [] ); |
| 568 |
|
| 569 |
// Restore saved selection, then apply execCommand |
| 570 |
const restoreAndExec = useCallback( ( command, arg = null ) => { |
| 571 |
editorRef.current?.focus(); |
| 572 |
if ( savedRangeRef.current ) { |
| 573 |
const sel = window.getSelection(); |
| 574 |
sel.removeAllRanges(); |
| 575 |
sel.addRange( savedRangeRef.current ); |
| 576 |
} |
| 577 |
document.execCommand( command, false, arg ); |
| 578 |
handleInput(); |
| 579 |
updateActiveState(); |
| 580 |
}, [ handleInput, updateActiveState ] ); |
| 581 |
|
| 582 |
const handleLinkSave = useCallback( ( href ) => { |
| 583 |
setLinkPopover( null ); |
| 584 |
if ( href && href !== 'https://' ) { |
| 585 |
restoreAndExec( 'createLink', href ); |
| 586 |
} |
| 587 |
}, [ restoreAndExec ] ); |
| 588 |
|
| 589 |
const handleLinkRemove = useCallback( () => { |
| 590 |
setLinkPopover( null ); |
| 591 |
// Select the full anchor node before unlinking |
| 592 |
const anchor = linkPopover?.anchorEl; |
| 593 |
if ( anchor && editorRef.current?.contains( anchor ) ) { |
| 594 |
editorRef.current.focus(); |
| 595 |
const range = document.createRange(); |
| 596 |
range.selectNode( anchor ); |
| 597 |
const sel = window.getSelection(); |
| 598 |
sel.removeAllRanges(); |
| 599 |
sel.addRange( range ); |
| 600 |
document.execCommand( 'unlink', false, null ); |
| 601 |
handleInput(); |
| 602 |
} |
| 603 |
}, [ linkPopover, handleInput ] ); |
| 604 |
|
| 605 |
const handleLinkEdit = useCallback( () => { |
| 606 |
setLinkPopover( ( prev ) => ( { ...prev, mode: 'insert' } ) ); |
| 607 |
}, [] ); |
| 608 |
|
| 609 |
const handleBlockChange = useCallback( ( e ) => { |
| 610 |
const tag = e.target.value; |
| 611 |
editorRef.current?.focus(); |
| 612 |
document.execCommand( 'formatBlock', false, tag ); |
| 613 |
setBlockValue( tag ); |
| 614 |
}, [] ); |
| 615 |
|
| 616 |
const BTNS = [ |
| 617 |
{ id: 'bold', icon: TB.bold, title: 'Bold (Ctrl+B)' }, |
| 618 |
{ id: 'italic', icon: TB.italic,title: 'Italic (Ctrl+I)' }, |
| 619 |
{ id: 'underline', icon: TB.under, title: 'Underline (Ctrl+U)' }, |
| 620 |
{ id: 'insertOrderedList', icon: TB.ol, title: 'Ordered list' }, |
| 621 |
{ id: 'insertUnorderedList', icon: TB.ul, title: 'Unordered list' }, |
| 622 |
{ id: 'removeFormat', icon: TB.clear, title: 'Clear formatting' }, |
| 623 |
{ id: 'link', icon: TB.link, title: 'Insert link', isLink: true }, |
| 624 |
]; |
| 625 |
|
| 626 |
return ( |
| 627 |
<div className="bp-cb-rich-text" ref={ containerRef }> |
| 628 |
{ /* Row 1: block style dropdown */ } |
| 629 |
<div className="bp-cb-rich-text__toolbar bp-cb-rich-text__toolbar--row1"> |
| 630 |
<select |
| 631 |
className="bp-cb-rich-text__block-select" |
| 632 |
value={ blockValue } |
| 633 |
onChange={ handleBlockChange } |
| 634 |
> |
| 635 |
{ BLOCKS.map( ( b ) => ( |
| 636 |
<option key={ b.value } value={ b.value }>{ b.label }</option> |
| 637 |
) ) } |
| 638 |
</select> |
| 639 |
</div> |
| 640 |
|
| 641 |
{ /* Row 2: format buttons */ } |
| 642 |
<div className="bp-cb-rich-text__toolbar bp-cb-rich-text__toolbar--row2"> |
| 643 |
{ BTNS.map( ( btn ) => ( |
| 644 |
<button |
| 645 |
key={ btn.id } |
| 646 |
type="button" |
| 647 |
title={ btn.title } |
| 648 |
className={ `bp-cb-rich-text__tool-btn${ activeFormats[ btn.id ] ? ' is-active' : '' }` } |
| 649 |
onMouseDown={ btn.isLink ? openLinkPopover : ( e ) => { e.preventDefault(); cmd( btn.id ); } } |
| 650 |
> |
| 651 |
{ btn.icon } |
| 652 |
</button> |
| 653 |
) ) } |
| 654 |
</div> |
| 655 |
|
| 656 |
{ /* Inline link popover */ } |
| 657 |
{ linkPopover && ( |
| 658 |
<div |
| 659 |
className={ `bp-cb-rich-text__link-popover bp-cb-rich-text__link-popover--${ linkPopover.mode }` } |
| 660 |
style={ { top: linkPopover.top } } |
| 661 |
onKeyDown={ ( e ) => { if ( e.key === 'Escape' ) setLinkPopover( null ); } } |
| 662 |
> |
| 663 |
{ linkPopover.mode === 'insert' ? ( |
| 664 |
<> |
| 665 |
<span className="bp-cb-rich-text__link-label"> |
| 666 |
{ __( 'Enter link:', 'better-payment' ) } |
| 667 |
</span> |
| 668 |
<input |
| 669 |
ref={ linkInputRef } |
| 670 |
type="url" |
| 671 |
className="bp-cb-rich-text__link-input" |
| 672 |
defaultValue={ linkPopover.href } |
| 673 |
onKeyDown={ ( e ) => { |
| 674 |
if ( e.key === 'Enter' ) { e.preventDefault(); handleLinkSave( e.target.value ); } |
| 675 |
} } |
| 676 |
/> |
| 677 |
<button |
| 678 |
type="button" |
| 679 |
className="bp-cb-rich-text__link-save" |
| 680 |
onMouseDown={ ( e ) => { e.preventDefault(); handleLinkSave( linkInputRef.current?.value ); } } |
| 681 |
> |
| 682 |
{ __( 'Save', 'better-payment' ) } |
| 683 |
</button> |
| 684 |
</> |
| 685 |
) : ( |
| 686 |
<> |
| 687 |
<a |
| 688 |
href={ linkPopover.href } |
| 689 |
className="bp-cb-rich-text__link-visit" |
| 690 |
target="_blank" |
| 691 |
rel="noreferrer" |
| 692 |
> |
| 693 |
{ linkPopover.href.length > 40 ? linkPopover.href.slice( 0, 40 ) + '…' : linkPopover.href } |
| 694 |
</a> |
| 695 |
<span className="bp-cb-rich-text__link-sep">|</span> |
| 696 |
<button type="button" className="bp-cb-rich-text__link-action" onMouseDown={ ( e ) => { e.preventDefault(); handleLinkEdit(); } }> |
| 697 |
{ __( 'Edit', 'better-payment' ) } |
| 698 |
</button> |
| 699 |
<span className="bp-cb-rich-text__link-sep">|</span> |
| 700 |
<button type="button" className="bp-cb-rich-text__link-action bp-cb-rich-text__link-action--remove" onMouseDown={ ( e ) => { e.preventDefault(); handleLinkRemove(); } }> |
| 701 |
{ __( 'Remove', 'better-payment' ) } |
| 702 |
</button> |
| 703 |
</> |
| 704 |
) } |
| 705 |
</div> |
| 706 |
) } |
| 707 |
|
| 708 |
<div |
| 709 |
ref={ editorRef } |
| 710 |
className="bp-cb-rich-text__editor" |
| 711 |
contentEditable |
| 712 |
suppressContentEditableWarning |
| 713 |
data-placeholder={ __( 'Write a description for your campaign…', 'better-payment' ) } |
| 714 |
onFocus={ () => { focusedRef.current = true; } } |
| 715 |
onBlur={ () => { focusedRef.current = false; handleInput(); } } |
| 716 |
onInput={ handleInput } |
| 717 |
onKeyUp={ updateActiveState } |
| 718 |
onMouseUp={ updateActiveState } |
| 719 |
/> |
| 720 |
</div> |
| 721 |
); |
| 722 |
} |
| 723 |
|
| 724 |
function ImageUploadControl( { value, onMultiChange, elementSettings } ) { |
| 725 |
const openMedia = useCallback( () => { |
| 726 |
openWpMedia( ( { url, id, sizes } ) => |
| 727 |
onMultiChange( { src: url, src_id: id, src_sizes: sizes } ) |
| 728 |
); |
| 729 |
}, [ onMultiChange ] ); |
| 730 |
|
| 731 |
const srcSizes = elementSettings?.src_sizes || {}; |
| 732 |
const size = elementSettings?.size || 'full'; |
| 733 |
const displaySrc = ( size !== 'full' && srcSizes[ size ]?.url ) ? srcSizes[ size ].url : ( value || '' ); |
| 734 |
const hasImage = !! displaySrc; |
| 735 |
|
| 736 |
return ( |
| 737 |
<div className="bp-cb-image-upload"> |
| 738 |
|
| 739 |
{ /* Elementor-style preview area */ } |
| 740 |
<div className="bp-cb-image-picker" onClick={ openMedia }> |
| 741 |
{ hasImage ? ( |
| 742 |
<> |
| 743 |
<img src={ displaySrc } alt="" className="bp-cb-image-picker__img" /> |
| 744 |
<button |
| 745 |
type="button" |
| 746 |
className="bp-cb-image-picker__delete" |
| 747 |
title={ __( 'Remove image', 'better-payment' ) } |
| 748 |
onClick={ ( e ) => { e.stopPropagation(); onMultiChange( { src: '', src_id: 0, src_sizes: {} } ); } } |
| 749 |
> |
| 750 |
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"> |
| 751 |
<path d="M2 4h12M5.333 4V2.667C5.333 2.313 5.473 1.974 5.724 1.724C5.974 1.473 6.313 1.333 6.667 1.333H9.333C9.687 1.333 10.026 1.473 10.276 1.724C10.527 1.974 10.667 2.313 10.667 2.667V4M12.667 4L12 13.333C12 13.687 11.86 14.026 11.609 14.276C11.359 14.527 11.02 14.667 10.667 14.667H5.333C4.98 14.667 4.641 14.527 4.391 14.276C4.14 14.026 4 13.687 4 13.333L3.333 4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/> |
| 752 |
</svg> |
| 753 |
</button> |
| 754 |
</> |
| 755 |
) : ( |
| 756 |
<div className="bp-cb-image-picker__placeholder"> |
| 757 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"> |
| 758 |
<circle cx="12" cy="12" r="11" stroke="currentColor" strokeWidth="1.5"/> |
| 759 |
<path d="M12 7v10M7 12h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/> |
| 760 |
</svg> |
| 761 |
</div> |
| 762 |
) } |
| 763 |
|
| 764 |
{ /* Choose Image bar */ } |
| 765 |
<div className="bp-cb-image-picker__bar" onClick={ ( e ) => e.stopPropagation() }> |
| 766 |
<button type="button" className="bp-cb-image-picker__choose" onClick={ openMedia }> |
| 767 |
{ __( 'Choose Image', 'better-payment' ) } |
| 768 |
</button> |
| 769 |
</div> |
| 770 |
</div> |
| 771 |
|
| 772 |
{ /* URL input + Clear */ } |
| 773 |
<input |
| 774 |
type="url" |
| 775 |
className="bp-cb-image-upload__url" |
| 776 |
value={ value ?? '' } |
| 777 |
placeholder="https://" |
| 778 |
onChange={ ( e ) => onMultiChange( { src: e.target.value, src_id: 0, src_sizes: {} } ) } |
| 779 |
/> |
| 780 |
{ hasImage && ( |
| 781 |
<button |
| 782 |
type="button" |
| 783 |
className="bp-cb-image-upload__btn bp-cb-image-upload__btn--clear" |
| 784 |
onClick={ () => onMultiChange( { src: '', src_id: 0, src_sizes: {} } ) } |
| 785 |
> |
| 786 |
{ __( 'Clear', 'better-payment' ) } |
| 787 |
</button> |
| 788 |
) } |
| 789 |
</div> |
| 790 |
); |
| 791 |
} |
| 792 |
|
| 793 |
function SettingsControl( { control, value, onChange, onMultiChange, elementSettings } ) { |
| 794 |
const { key, label, type, placeholder, min, max, rows, options, info, unit, step } = control; |
| 795 |
|
| 796 |
if ( type === 'section_label' ) { |
| 797 |
return ( |
| 798 |
<div className="bp-cb-section-label"> |
| 799 |
<span>{ label }</span> |
| 800 |
</div> |
| 801 |
); |
| 802 |
} |
| 803 |
|
| 804 |
if ( type === 'note' ) { |
| 805 |
return <p className="bp-cb-widget-note">{ label }</p>; |
| 806 |
} |
| 807 |
|
| 808 |
return ( |
| 809 |
<div className="bp-cb-control"> |
| 810 |
{ type !== 'toggle' && type !== 'switch' && type !== 'image_upload' && type !== 'rich_text' && ( |
| 811 |
<label className="bp-cb-field-label" htmlFor={ `ctrl-${ key }` }> |
| 812 |
{ label } |
| 813 |
</label> |
| 814 |
) } |
| 815 |
|
| 816 |
{ type === 'image_upload' && ( |
| 817 |
<> |
| 818 |
<label className="bp-cb-field-label"> |
| 819 |
{ label } |
| 820 |
</label> |
| 821 |
<ImageUploadControl value={ value } onMultiChange={ onMultiChange } elementSettings={ elementSettings } /> |
| 822 |
</> |
| 823 |
) } |
| 824 |
|
| 825 |
{ type === 'rich_text' && ( |
| 826 |
<> |
| 827 |
<label className="bp-cb-field-label"> |
| 828 |
{ label } |
| 829 |
</label> |
| 830 |
<RichTextControl value={ value } onChange={ onChange } /> |
| 831 |
</> |
| 832 |
) } |
| 833 |
|
| 834 |
{ type === 'range' && ( |
| 835 |
<div className="bp-cb-range-wrap"> |
| 836 |
<input |
| 837 |
id={ `ctrl-${ key }` } |
| 838 |
type="range" |
| 839 |
min={ min ?? 10 } |
| 840 |
max={ max ?? 100 } |
| 841 |
step={ step ?? 1 } |
| 842 |
value={ value ?? ( control.defaultValue ?? 100 ) } |
| 843 |
onChange={ ( e ) => onChange( parseInt( e.target.value, 10 ) ) } |
| 844 |
/> |
| 845 |
<span className="bp-cb-range-value"> |
| 846 |
{ value ?? ( control.defaultValue ?? 100 ) }{ unit || '%' } |
| 847 |
</span> |
| 848 |
</div> |
| 849 |
) } |
| 850 |
|
| 851 |
{ type === 'align' && ( |
| 852 |
<div className="bp-cb-align-group"> |
| 853 |
{ ALIGN_OPTIONS.map( ( opt ) => ( |
| 854 |
<button |
| 855 |
key={ opt.value } |
| 856 |
type="button" |
| 857 |
title={ opt.title } |
| 858 |
className={ `bp-cb-align-btn${ ( value || 'left' ) === opt.value ? ' is-active' : '' }` } |
| 859 |
onClick={ () => onChange( opt.value ) } |
| 860 |
> |
| 861 |
{ opt.icon } |
| 862 |
</button> |
| 863 |
) ) } |
| 864 |
</div> |
| 865 |
) } |
| 866 |
|
| 867 |
{ type === 'text' && ( |
| 868 |
<input |
| 869 |
id={ `ctrl-${ key }` } |
| 870 |
type="text" |
| 871 |
value={ value ?? '' } |
| 872 |
placeholder={ placeholder || '' } |
| 873 |
onChange={ ( e ) => onChange( e.target.value ) } |
| 874 |
/> |
| 875 |
) } |
| 876 |
|
| 877 |
{ type === 'url' && ( |
| 878 |
<input |
| 879 |
id={ `ctrl-${ key }` } |
| 880 |
type="url" |
| 881 |
value={ value ?? '' } |
| 882 |
placeholder={ placeholder || 'https://' } |
| 883 |
onChange={ ( e ) => onChange( e.target.value ) } |
| 884 |
/> |
| 885 |
) } |
| 886 |
|
| 887 |
{ type === 'number' && ( |
| 888 |
<input |
| 889 |
id={ `ctrl-${ key }` } |
| 890 |
type="number" |
| 891 |
value={ value ?? '' } |
| 892 |
min={ min } |
| 893 |
max={ max } |
| 894 |
onChange={ ( e ) => onChange( parseInt( e.target.value, 10 ) ) } |
| 895 |
/> |
| 896 |
) } |
| 897 |
|
| 898 |
{ type === 'color' && ( |
| 899 |
<div className="bp-color-row"> |
| 900 |
<input |
| 901 |
id={ `ctrl-${ key }` } |
| 902 |
type="color" |
| 903 |
value={ value || '#6b63f6' } |
| 904 |
onChange={ ( e ) => onChange( e.target.value ) } |
| 905 |
/> |
| 906 |
<span className="bp-color-value">{ value || '#6b63f6' }</span> |
| 907 |
</div> |
| 908 |
) } |
| 909 |
|
| 910 |
{ type === 'toggle' && ( |
| 911 |
<label className="bp-cb-toggle-label" htmlFor={ `ctrl-${ key }` }> |
| 912 |
<input |
| 913 |
id={ `ctrl-${ key }` } |
| 914 |
type="checkbox" |
| 915 |
checked={ !! value } |
| 916 |
onChange={ ( e ) => onChange( e.target.checked ) } |
| 917 |
/> |
| 918 |
<span>{ label }</span> |
| 919 |
</label> |
| 920 |
) } |
| 921 |
|
| 922 |
{ type === 'switch' && ( |
| 923 |
<label className="bp-cb-switch-label" htmlFor={ `ctrl-${ key }` }> |
| 924 |
<span className="bp-cb-switch-wrap"> |
| 925 |
<input |
| 926 |
id={ `ctrl-${ key }` } |
| 927 |
type="checkbox" |
| 928 |
checked={ !! value } |
| 929 |
onChange={ ( e ) => onChange( e.target.checked ) } |
| 930 |
/> |
| 931 |
<span className="bp-cb-switch-track" /> |
| 932 |
</span> |
| 933 |
<span>{ label }</span> |
| 934 |
</label> |
| 935 |
) } |
| 936 |
|
| 937 |
{ type === 'select' && ( |
| 938 |
<select |
| 939 |
id={ `ctrl-${ key }` } |
| 940 |
value={ value ?? '' } |
| 941 |
onChange={ ( e ) => onChange( e.target.value ) } |
| 942 |
> |
| 943 |
{ ( options || [] ).map( ( opt ) => ( |
| 944 |
<option key={ opt.value } value={ opt.value }>{ opt.label }</option> |
| 945 |
) ) } |
| 946 |
</select> |
| 947 |
) } |
| 948 |
|
| 949 |
{ type === 'textarea' && ( |
| 950 |
<textarea |
| 951 |
id={ `ctrl-${ key }` } |
| 952 |
rows={ rows || 4 } |
| 953 |
value={ value ?? '' } |
| 954 |
placeholder={ placeholder || '' } |
| 955 |
onChange={ ( e ) => onChange( e.target.value ) } |
| 956 |
/> |
| 957 |
) } |
| 958 |
|
| 959 |
{ type === 'suggested_amounts' && ( |
| 960 |
<SuggestedAmountsEditor |
| 961 |
items={ Array.isArray( value ) ? value : [] } |
| 962 |
onChange={ onChange } |
| 963 |
compact |
| 964 |
/> |
| 965 |
) } |
| 966 |
</div> |
| 967 |
); |
| 968 |
} |
| 969 |
|