| 1 |
/** |
| 2 |
* Renders one option from its schema entry. |
| 3 |
* |
| 4 |
* Every branch here maps a type from SettingsSchema.php to a control. The help |
| 5 |
* text is never optional -- it comes from the schema, and the schema requires |
| 6 |
* one for every field. |
| 7 |
*/ |
| 8 |
|
| 9 |
import { __ } from '@wordpress/i18n'; |
| 10 |
import { useState } from '@wordpress/element'; |
| 11 |
import { |
| 12 |
BaseControl, |
| 13 |
Button, |
| 14 |
RadioControl, |
| 15 |
SelectControl, |
| 16 |
TextControl, |
| 17 |
TextareaControl, |
| 18 |
ToggleControl, |
| 19 |
__experimentalVStack as VStack, |
| 20 |
} from '@wordpress/components'; |
| 21 |
|
| 22 |
/** |
| 23 |
* Whether a field's condition is met by the current values. |
| 24 |
* |
| 25 |
* @param {Object} field The schema entry. |
| 26 |
* @param {Object} values Current values. |
| 27 |
* @return {boolean} True when the field should be shown. |
| 28 |
*/ |
| 29 |
export function isVisible( field, values ) { |
| 30 |
if ( ! field.depends ) { |
| 31 |
return true; |
| 32 |
} |
| 33 |
|
| 34 |
return Object.entries( field.depends ).every( |
| 35 |
// Loose comparison: a choice value of 1 arrives as a number from the |
| 36 |
// schema but may sit in the form state as the string "1". |
| 37 |
// eslint-disable-next-line eqeqeq |
| 38 |
( [ key, expected ] ) => values[ key ] == expected |
| 39 |
); |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* Opens the media library and hands back the chosen attachment. |
| 44 |
* |
| 45 |
* @param {Function} onSelect Receives the attachment id and its preview url. |
| 46 |
*/ |
| 47 |
function openMediaLibrary( onSelect ) { |
| 48 |
if ( ! window.wp?.media ) { |
| 49 |
return; |
| 50 |
} |
| 51 |
|
| 52 |
const frame = window.wp.media( { |
| 53 |
title: __( 'Choose an image', 'cryptx' ), |
| 54 |
multiple: false, |
| 55 |
library: { type: 'image' }, |
| 56 |
} ); |
| 57 |
|
| 58 |
frame.on( 'select', () => { |
| 59 |
const attachment = frame.state().get( 'selection' ).first().toJSON(); |
| 60 |
|
| 61 |
onSelect( attachment.id, attachment.url ); |
| 62 |
} ); |
| 63 |
|
| 64 |
frame.open(); |
| 65 |
} |
| 66 |
|
| 67 |
export default function Field( { field, value, onChange } ) { |
| 68 |
const [ mediaUrl, setMediaUrl ] = useState( null ); |
| 69 |
const id = `cryptx-field-${ field.key }`; |
| 70 |
|
| 71 |
switch ( field.type ) { |
| 72 |
case 'boolean': |
| 73 |
return ( |
| 74 |
<ToggleControl |
| 75 |
__nextHasNoMarginBottom |
| 76 |
label={ field.label } |
| 77 |
help={ field.help } |
| 78 |
checked={ !! Number( value ) } |
| 79 |
onChange={ ( checked ) => |
| 80 |
onChange( field.key, checked ? 1 : 0 ) |
| 81 |
} |
| 82 |
/> |
| 83 |
); |
| 84 |
|
| 85 |
case 'choice': { |
| 86 |
const options = field.choices.map( ( choice ) => ( { |
| 87 |
label: choice.label, |
| 88 |
value: String( choice.value ), |
| 89 |
} ) ); |
| 90 |
|
| 91 |
// Radio buttons show every option and its consequence at a glance, |
| 92 |
// which matters for the security choices. Beyond four they turn |
| 93 |
// into a wall, so those become a select. |
| 94 |
if ( options.length <= 4 ) { |
| 95 |
return ( |
| 96 |
<RadioControl |
| 97 |
label={ field.label } |
| 98 |
help={ field.help } |
| 99 |
selected={ String( value ) } |
| 100 |
options={ options } |
| 101 |
onChange={ ( next ) => onChange( field.key, next ) } |
| 102 |
/> |
| 103 |
); |
| 104 |
} |
| 105 |
|
| 106 |
return ( |
| 107 |
<SelectControl |
| 108 |
__nextHasNoMarginBottom |
| 109 |
__next40pxDefaultSize |
| 110 |
label={ field.label } |
| 111 |
help={ field.help } |
| 112 |
value={ String( value ) } |
| 113 |
options={ options } |
| 114 |
onChange={ ( next ) => onChange( field.key, next ) } |
| 115 |
/> |
| 116 |
); |
| 117 |
} |
| 118 |
|
| 119 |
case 'integer': |
| 120 |
return ( |
| 121 |
<TextControl |
| 122 |
__nextHasNoMarginBottom |
| 123 |
__next40pxDefaultSize |
| 124 |
type="number" |
| 125 |
min={ field.min } |
| 126 |
max={ field.max } |
| 127 |
label={ field.label } |
| 128 |
help={ field.help } |
| 129 |
value={ value } |
| 130 |
onChange={ ( next ) => onChange( field.key, next ) } |
| 131 |
/> |
| 132 |
); |
| 133 |
|
| 134 |
case 'idlist': |
| 135 |
case 'addresslist': |
| 136 |
return ( |
| 137 |
<TextareaControl |
| 138 |
__nextHasNoMarginBottom |
| 139 |
label={ field.label } |
| 140 |
help={ field.help } |
| 141 |
value={ value } |
| 142 |
rows={ 3 } |
| 143 |
onChange={ ( next ) => onChange( field.key, next ) } |
| 144 |
/> |
| 145 |
); |
| 146 |
|
| 147 |
case 'color': |
| 148 |
return ( |
| 149 |
<BaseControl |
| 150 |
__nextHasNoMarginBottom |
| 151 |
id={ id } |
| 152 |
label={ field.label } |
| 153 |
help={ field.help } |
| 154 |
> |
| 155 |
<div className="cryptx-color"> |
| 156 |
{ /* A native colour input works on every device, |
| 157 |
including the phone's own colour picker, and needs |
| 158 |
no extra bundle. */ } |
| 159 |
<input |
| 160 |
id={ id } |
| 161 |
type="color" |
| 162 |
value={ value || '#000000' } |
| 163 |
onChange={ ( event ) => |
| 164 |
onChange( field.key, event.target.value ) |
| 165 |
} |
| 166 |
/> |
| 167 |
<TextControl |
| 168 |
__nextHasNoMarginBottom |
| 169 |
__next40pxDefaultSize |
| 170 |
label={ __( 'Hex value', 'cryptx' ) } |
| 171 |
hideLabelFromVision |
| 172 |
value={ value } |
| 173 |
onChange={ ( next ) => onChange( field.key, next ) } |
| 174 |
/> |
| 175 |
</div> |
| 176 |
</BaseControl> |
| 177 |
); |
| 178 |
|
| 179 |
case 'media': |
| 180 |
return ( |
| 181 |
<BaseControl |
| 182 |
__nextHasNoMarginBottom |
| 183 |
id={ id } |
| 184 |
label={ field.label } |
| 185 |
help={ field.help } |
| 186 |
> |
| 187 |
<VStack spacing={ 2 } className="cryptx-media"> |
| 188 |
{ mediaUrl && ( |
| 189 |
<img |
| 190 |
src={ mediaUrl } |
| 191 |
alt="" |
| 192 |
className="cryptx-media__preview" |
| 193 |
/> |
| 194 |
) } |
| 195 |
<div className="cryptx-media__buttons"> |
| 196 |
<Button |
| 197 |
variant="secondary" |
| 198 |
__next40pxDefaultSize |
| 199 |
onClick={ () => |
| 200 |
openMediaLibrary( ( attachmentId, url ) => { |
| 201 |
onChange( field.key, attachmentId ); |
| 202 |
setMediaUrl( url ); |
| 203 |
} ) |
| 204 |
} |
| 205 |
> |
| 206 |
{ Number( value ) |
| 207 |
? __( 'Replace image', 'cryptx' ) |
| 208 |
: __( 'Choose image', 'cryptx' ) } |
| 209 |
</Button> |
| 210 |
{ !! Number( value ) && ( |
| 211 |
<Button |
| 212 |
variant="tertiary" |
| 213 |
isDestructive |
| 214 |
__next40pxDefaultSize |
| 215 |
onClick={ () => { |
| 216 |
onChange( field.key, 0 ); |
| 217 |
setMediaUrl( null ); |
| 218 |
} } |
| 219 |
> |
| 220 |
{ __( 'Remove', 'cryptx' ) } |
| 221 |
</Button> |
| 222 |
) } |
| 223 |
</div> |
| 224 |
</VStack> |
| 225 |
</BaseControl> |
| 226 |
); |
| 227 |
|
| 228 |
case 'url': |
| 229 |
return ( |
| 230 |
<TextControl |
| 231 |
__nextHasNoMarginBottom |
| 232 |
__next40pxDefaultSize |
| 233 |
type="url" |
| 234 |
label={ field.label } |
| 235 |
help={ field.help } |
| 236 |
value={ value } |
| 237 |
onChange={ ( next ) => onChange( field.key, next ) } |
| 238 |
/> |
| 239 |
); |
| 240 |
|
| 241 |
case 'htmlid': |
| 242 |
case 'string': |
| 243 |
default: |
| 244 |
return ( |
| 245 |
<TextControl |
| 246 |
__nextHasNoMarginBottom |
| 247 |
__next40pxDefaultSize |
| 248 |
label={ field.label } |
| 249 |
help={ field.help } |
| 250 |
value={ value } |
| 251 |
onChange={ ( next ) => onChange( field.key, next ) } |
| 252 |
/> |
| 253 |
); |
| 254 |
} |
| 255 |
} |
| 256 |
|