| 1 |
(function(wp, _, panels) { |
| 2 |
const el = (tag, ...args)=> { |
| 3 |
return wp.element.createElement(typeof tag === 'string' && /^[A-Z]/.test(tag) ? wp.components[tag] : tag, ...args); |
| 4 |
}; |
| 5 |
|
| 6 |
const isEqual = (a, b)=> String(a) === String(b); |
| 7 |
|
| 8 |
const parseName = (name)=> { |
| 9 |
const parts = name.includes('[') && !name.startsWith('[') && name.endsWith(']') && name.split(/(\[|\])/).filter(t => t !== ''); |
| 10 |
|
| 11 |
if(!parts || parts.length % 3 !== 1){ |
| 12 |
return [name]; |
| 13 |
} |
| 14 |
|
| 15 |
const names = [parts[0]]; |
| 16 |
|
| 17 |
for(let i = 1; i < parts.length; i += 3){ |
| 18 |
if(parts[i] !== '[' || !parts[i+1] || parts[i+2] !== ']'){ |
| 19 |
return [name]; |
| 20 |
} |
| 21 |
|
| 22 |
names.push(parts[i+1]); |
| 23 |
} |
| 24 |
|
| 25 |
return names; |
| 26 |
}; |
| 27 |
|
| 28 |
const getValue = (name, {data, value_callback})=> { |
| 29 |
const names = parseName(name); |
| 30 |
const val = names.reduce((acc, n)=> acc?.[n], value_callback ? {[names[0]]: value_callback(names[0])} : data); |
| 31 |
|
| 32 |
return val !== undefined ? val : null; |
| 33 |
}; |
| 34 |
|
| 35 |
const setValue = (name, value, args)=> { |
| 36 |
const names = parseName(name); |
| 37 |
const data = { [names[0]]: getValue(names[0], args) }; |
| 38 |
|
| 39 |
names.reduce((acc, n, i)=> { |
| 40 |
if(i === names.length - 1){ |
| 41 |
if(value === null && names.length > 1){ |
| 42 |
delete acc[n]; |
| 43 |
}else{ |
| 44 |
acc[n] = value; |
| 45 |
} |
| 46 |
}else{ |
| 47 |
acc[n] = _.isArray(acc[n]) ? [...acc[n]] : (_.isObject(acc[n]) ? {...acc[n]} : {}); |
| 48 |
} |
| 49 |
|
| 50 |
return acc[n]; |
| 51 |
}, data); |
| 52 |
|
| 53 |
return data; |
| 54 |
}; |
| 55 |
|
| 56 |
const sanitizeValue = (schema, value)=> { |
| 57 |
if(value === ''){ |
| 58 |
return null; |
| 59 |
} |
| 60 |
|
| 61 |
const type = schema.type; |
| 62 |
|
| 63 |
if(type === 'array'){ |
| 64 |
return value.map(v => sanitizeValue(schema.items, v)).filter(v => v !== null); |
| 65 |
}else if(['integer', 'number'].includes(type)){ |
| 66 |
value = type === 'integer' ? parseInt(value, 10) : parseFloat(value); |
| 67 |
|
| 68 |
return isNaN(value) ? null : value; |
| 69 |
}else if(type === 'string'){ |
| 70 |
return value === null ? '' : String(value); |
| 71 |
} |
| 72 |
|
| 73 |
return value; |
| 74 |
}; |
| 75 |
|
| 76 |
const validateValue = (schema, value, prevValue)=> { |
| 77 |
if(!schema || !_.isArray(value)){ |
| 78 |
return null; |
| 79 |
} |
| 80 |
|
| 81 |
if(schema.maxItems && value.length > schema.maxItems && (!prevValue || value.length > prevValue.length)){ |
| 82 |
return '最多支持'+schema.maxItems+'个'; |
| 83 |
} |
| 84 |
|
| 85 |
if(schema.minItems && value.length < schema.minItems && prevValue && value.length < prevValue.length){ |
| 86 |
return '至少需要'+schema.minItems+'个'; |
| 87 |
} |
| 88 |
|
| 89 |
if(schema.uniqueItems && _.uniq(value.map(String)).length !== value.length){ |
| 90 |
return '不� |
| 91 |
�许重复'; |
| 92 |
} |
| 93 |
|
| 94 |
return null; |
| 95 |
}; |
| 96 |
|
| 97 |
const TruncateText = (text)=> el('span', {className: 'truncate-text'}, text); |
| 98 |
|
| 99 |
const MediaButton = ({field, render})=> { |
| 100 |
const isId = field.item_type === 'id'; |
| 101 |
const allowed = field.item_type ? { allowedTypes: [isId ? 'image' : field.item_type] } : {}; |
| 102 |
|
| 103 |
return el(wp.blockEditor.MediaUpload, { |
| 104 |
...allowed, |
| 105 |
multiple: !!field.mu, |
| 106 |
onSelect: (media)=> { |
| 107 |
const toVal = (m)=> isId ? m.id : m.url; |
| 108 |
|
| 109 |
field.mu ? field.mu.addItem(...media.map(toVal)) : field.update(toVal(media)); |
| 110 |
}, |
| 111 |
render: render || (({open})=> el('Button', { |
| 112 |
variant: 'secondary', |
| 113 |
onClick: open |
| 114 |
}, field.button_text)) |
| 115 |
}); |
| 116 |
}; |
| 117 |
|
| 118 |
const FileControl = ({field})=> { |
| 119 |
const {value, update, mu} = field; |
| 120 |
const [newVal, setNewVal] = wp.element.useState(''); |
| 121 |
|
| 122 |
const length = value.length; |
| 123 |
|
| 124 |
const textInput = (val, index)=> el('TextControl', { |
| 125 |
value: val, |
| 126 |
placeholder: field.placeholder || 'https://...', |
| 127 |
__next40pxDefaultSize: true, |
| 128 |
...(mu ? { |
| 129 |
onChange: (v)=> { |
| 130 |
if(index === length){ |
| 131 |
setNewVal(v); |
| 132 |
}else{ |
| 133 |
mu.updateItem(index, v); |
| 134 |
} |
| 135 |
}, |
| 136 |
onBlur: ()=> { |
| 137 |
if(index === length){ |
| 138 |
if(val){ |
| 139 |
mu.addItem(val); |
| 140 |
setNewVal(''); |
| 141 |
} |
| 142 |
}else if(!val){ |
| 143 |
mu.removeItem(index); |
| 144 |
} |
| 145 |
} |
| 146 |
} : { |
| 147 |
onChange: (v)=> update(v || null) |
| 148 |
}) |
| 149 |
}); |
| 150 |
|
| 151 |
if(mu){ |
| 152 |
return el('Flex', { |
| 153 |
direction: 'column', |
| 154 |
align: 'flex-start', |
| 155 |
gap: 2 |
| 156 |
}, [...value, newVal].map((val, index) => el('Flex', { |
| 157 |
...(index < length ? {...mu.dragProps, 'data-index': index} : {}), |
| 158 |
className: 'mu-item', |
| 159 |
key: index, |
| 160 |
gap: 1 |
| 161 |
}, el('FlexBlock', null, textInput(val, index)), index < length ? el('Button', { |
| 162 |
variant: 'secondary', |
| 163 |
isDestructive: true, |
| 164 |
onClick: ()=> mu.removeItem(index) |
| 165 |
}, '删除') : null, index < length ? el('Button', { |
| 166 |
icon: 'menu', |
| 167 |
size: 'small', |
| 168 |
className: 'move-item' |
| 169 |
}) : el(MediaButton, { |
| 170 |
field |
| 171 |
})))); |
| 172 |
} |
| 173 |
|
| 174 |
return el('Flex', { |
| 175 |
justify: 'flex-start', |
| 176 |
gap: 1 |
| 177 |
}, el('FlexBlock', null, textInput(value)), el(MediaButton, { |
| 178 |
field |
| 179 |
})); |
| 180 |
}; |
| 181 |
|
| 182 |
const MediaControl = ({field})=> { |
| 183 |
const [urlInput, setUrlInput] = wp.element.useState(null); |
| 184 |
|
| 185 |
const value = field.value; |
| 186 |
const isId = field.item_type === 'id'; |
| 187 |
const mu = field.mu; |
| 188 |
let urls = mu ? value : (value ? [value] : []); |
| 189 |
const ids = isId ? urls : []; |
| 190 |
const cache = wp.element.useRef({}); |
| 191 |
const data = wp.data.useSelect(select => { |
| 192 |
const missing = ids.filter(id => !cache.current[id]); |
| 193 |
return missing.length ? select('core').getEntityRecords('postType', 'attachment', { |
| 194 |
include: missing, |
| 195 |
per_page: missing.length |
| 196 |
}) || [] : []; |
| 197 |
}, [JSON.stringify(ids)]) |
| 198 |
|
| 199 |
if(isId){ |
| 200 |
data.forEach(m => cache.current[m.id] = m.source_url); |
| 201 |
urls = ids.map(id => cache.current[id]).filter(Boolean); |
| 202 |
} |
| 203 |
|
| 204 |
if(urlInput !== null){ |
| 205 |
return el('Flex', { |
| 206 |
direction: 'column' |
| 207 |
}, el('TextControl', { |
| 208 |
value: urlInput, |
| 209 |
placeholder: 'https://...', |
| 210 |
onChange: (val)=> setUrlInput(val) |
| 211 |
}), el('Flex', { |
| 212 |
justify: 'flex-start' |
| 213 |
}, el('Button', { |
| 214 |
variant: 'primary', |
| 215 |
onClick: ()=> { |
| 216 |
urlInput && (mu ? mu.addItem(urlInput) : field.update(urlInput)); |
| 217 |
|
| 218 |
setUrlInput(null); |
| 219 |
} |
| 220 |
}, '应用'), el('Button', { |
| 221 |
variant: 'secondary', |
| 222 |
onClick: ()=> setUrlInput(null) |
| 223 |
}, '取消'))); |
| 224 |
} |
| 225 |
|
| 226 |
if(mu){ |
| 227 |
return el('Flex', { |
| 228 |
className: 'mu-img', |
| 229 |
justify: 'flex-start', |
| 230 |
wrap: true, |
| 231 |
gap: 2 |
| 232 |
}, urls.map((src, i) => { |
| 233 |
return el('div', { |
| 234 |
...mu.dragProps, |
| 235 |
'data-index': i, |
| 236 |
key: i, |
| 237 |
className: 'mu-item' |
| 238 |
}, el('img', { |
| 239 |
src |
| 240 |
}), el('Button', { |
| 241 |
icon: 'no-alt', |
| 242 |
size: 'small', |
| 243 |
className: 'del-img', |
| 244 |
onClick: ()=> mu.removeItem(i) |
| 245 |
})); |
| 246 |
}), el(MediaButton, { |
| 247 |
field, |
| 248 |
render: ({open})=> el('div', { |
| 249 |
className: 'new-item', |
| 250 |
onClick: open |
| 251 |
}, (isId ? '' : el('Button', { |
| 252 |
icon: 'admin-links', |
| 253 |
size: 'small', |
| 254 |
onClick: (e)=> { |
| 255 |
e.stopPropagation(); |
| 256 |
setUrlInput(''); |
| 257 |
} |
| 258 |
}))) |
| 259 |
})); |
| 260 |
} |
| 261 |
|
| 262 |
if(urls[0]){ |
| 263 |
return el('div', { |
| 264 |
className: 'wpjam-img' |
| 265 |
}, el(MediaButton, { |
| 266 |
field, |
| 267 |
render: ({open})=> el('img', { |
| 268 |
src: urls[0], |
| 269 |
onClick: open |
| 270 |
}) |
| 271 |
}), el('Button', { |
| 272 |
icon: 'no-alt', |
| 273 |
className: 'del-img', |
| 274 |
onClick: ()=> field.update(null) |
| 275 |
})); |
| 276 |
} |
| 277 |
|
| 278 |
return el('Flex', { |
| 279 |
justify: 'flex-start' |
| 280 |
}, el(MediaButton, { |
| 281 |
field, |
| 282 |
render: ({open})=> el('Button', { |
| 283 |
variant: 'secondary', |
| 284 |
icon: 'camera', |
| 285 |
text: field.button_text, |
| 286 |
onClick: open |
| 287 |
}) |
| 288 |
}), (isId ? '' : el('Button', { |
| 289 |
variant: 'secondary', |
| 290 |
text: '输� |
| 291 |
�外链', |
| 292 |
onClick: ()=> setUrlInput(value || '') |
| 293 |
}))); |
| 294 |
}; |
| 295 |
|
| 296 |
const UploaderControl = ({field})=> { |
| 297 |
const [uploading, setUploading] = wp.element.useState(false); |
| 298 |
|
| 299 |
return el('Flex', { |
| 300 |
justify: 'flex-start', |
| 301 |
gap: 1 |
| 302 |
}, el('FormFileUpload', { |
| 303 |
accept: field.accept, |
| 304 |
disabled: uploading, |
| 305 |
variant: 'secondary', |
| 306 |
onChange: (e)=> { |
| 307 |
const file = e.target.files[0]; |
| 308 |
|
| 309 |
if(!file) return; |
| 310 |
|
| 311 |
setUploading(true); |
| 312 |
|
| 313 |
const reader = new FileReader(); |
| 314 |
|
| 315 |
reader.onerror = ()=> { |
| 316 |
setUploading(false); |
| 317 |
alert('文件读取失败'); |
| 318 |
}; |
| 319 |
|
| 320 |
reader.onload = ()=> { |
| 321 |
wpjam.post({ |
| 322 |
action: 'wpjam-upload', |
| 323 |
name: field.name, |
| 324 |
filename: file.name, |
| 325 |
bits: reader.result, |
| 326 |
_ajax_nonce: field.nonce |
| 327 |
}).then(data => { |
| 328 |
setUploading(false); |
| 329 |
|
| 330 |
if(data.errcode === 0){ |
| 331 |
field.update(data.url); |
| 332 |
}else{ |
| 333 |
alert(data.errmsg || '上传失败'); |
| 334 |
} |
| 335 |
}); |
| 336 |
}; |
| 337 |
|
| 338 |
reader.readAsDataURL(file); |
| 339 |
} |
| 340 |
}, field.button_text), (field.value ? el('FlexBlock', null, el('Flex', { |
| 341 |
justify: 'flex-start', |
| 342 |
wrap: false, |
| 343 |
gap: 0 |
| 344 |
}, el('Button', { |
| 345 |
icon: 'dismiss', |
| 346 |
size: 'small', |
| 347 |
onClick: ()=> field.update(null) |
| 348 |
}), TruncateText(field.value.split('/').pop()))) : '')); |
| 349 |
}; |
| 350 |
|
| 351 |
const ColorControl = ({field})=> { |
| 352 |
let value = field.value || '#000000'; |
| 353 |
|
| 354 |
return el('Dropdown', { |
| 355 |
renderToggle: ({isOpen, onToggle})=> el('Button', { |
| 356 |
onClick: onToggle, |
| 357 |
style: { |
| 358 |
color: value, |
| 359 |
border: `1px solid ${value}` |
| 360 |
} |
| 361 |
}, el('ColorIndicator', { |
| 362 |
colorValue: value |
| 363 |
}), value || field.button_text || wp.i18n.__('Select Color', 'default')), |
| 364 |
renderContent: ()=> el('ColorPicker', { |
| 365 |
color: value, |
| 366 |
enableAlpha: field.alpha || false, |
| 367 |
onChange: (color)=> field.update(color) |
| 368 |
}) |
| 369 |
}); |
| 370 |
}; |
| 371 |
|
| 372 |
const MuSelectControl = ({field})=> { |
| 373 |
return el('Dropdown', { |
| 374 |
focusOnMount: false, |
| 375 |
popoverProps: { className: 'components-wpjam-select-popover' }, |
| 376 |
renderToggle: ({isOpen, onToggle})=> el('Button', { |
| 377 |
icon: 'arrow-down-alt2', |
| 378 |
iconPosition: 'right', |
| 379 |
iconSize: 12, |
| 380 |
className: 'truncate-text', |
| 381 |
onClick: onToggle, |
| 382 |
}, field.value.length > 0 ? field.value.map(v => (field.options.find(o => isEqual(o.value, v)) || {}).label || v).join(', ') : (field.placeholder || '请选择')), |
| 383 |
renderContent: ()=> el(CheckboxControl, {field}) |
| 384 |
}); |
| 385 |
}; |
| 386 |
|
| 387 |
const ImageRadioControl = ({field})=> { |
| 388 |
return el('Flex', { |
| 389 |
justify: 'flex-start', |
| 390 |
wrap: true, |
| 391 |
gap: 4, |
| 392 |
}, field.options.map(opt => { |
| 393 |
return el('label', { |
| 394 |
key: opt.value, |
| 395 |
className: 'image-radio' |
| 396 |
}, el('input', { |
| 397 |
type: 'radio', |
| 398 |
name: field.name, |
| 399 |
value: opt.value, |
| 400 |
checked: isEqual(field.value, opt.value), |
| 401 |
onChange: ()=> field.update(opt.value) |
| 402 |
}), [].concat(opt.image).slice(0, 2).map((src, i) => el('img', { |
| 403 |
key: i, |
| 404 |
src, |
| 405 |
alt: opt.label |
| 406 |
})), opt.label); |
| 407 |
})); |
| 408 |
}; |
| 409 |
|
| 410 |
const CheckboxControl = ({field})=> { |
| 411 |
return el('Flex', { |
| 412 |
direction: 'column', |
| 413 |
gap: 3, |
| 414 |
}, field.options.map(opt => el('CheckboxControl', { |
| 415 |
key: opt.value, |
| 416 |
label: opt.label, |
| 417 |
checked: field.value.some(v => isEqual(v, opt.value)), |
| 418 |
onChange: (checked)=> field.update(checked ? [...field.value, opt.value] : field.value.filter(v=> !isEqual(v, opt.value))) |
| 419 |
}))); |
| 420 |
}; |
| 421 |
|
| 422 |
const ComboboxControl = ({field})=> { |
| 423 |
const [options, setOptions] = wp.element.useState([]); |
| 424 |
const [comboValue, setComboValue] = wp.element.useState(''); |
| 425 |
|
| 426 |
const labels = wp.element.useRef({}); |
| 427 |
const ref = wp.element.useRef(null); |
| 428 |
|
| 429 |
const getLabel = (v)=> labels.current[String(v)] || String(v); |
| 430 |
const queryItems = ({search, include, exclude})=> { |
| 431 |
const query_args = field.query_args || {}; |
| 432 |
|
| 433 |
if(search){ |
| 434 |
query_args[field.data_type === 'post_type' ? 's' : 'search'] = search; |
| 435 |
} |
| 436 |
|
| 437 |
if(exclude?.length){ |
| 438 |
query_args.exclude = exclude; |
| 439 |
} |
| 440 |
|
| 441 |
wpjam.post({ |
| 442 |
action: 'wpjam-query', |
| 443 |
data_type: field.data_type, |
| 444 |
query_args: query_args, |
| 445 |
...(include ? {include} : {}) |
| 446 |
}).then(data => { |
| 447 |
if(data.errcode === 0){ |
| 448 |
data.items.forEach(o => labels.current[String(o.value)] = o.label); |
| 449 |
|
| 450 |
setOptions(data.items); |
| 451 |
} |
| 452 |
}); |
| 453 |
}; |
| 454 |
|
| 455 |
let {value, mu, update} = field; |
| 456 |
|
| 457 |
if(!mu && value !== null){ |
| 458 |
value = String(value); |
| 459 |
} |
| 460 |
|
| 461 |
wp.element.useEffect(()=> { |
| 462 |
if(!value || !ref.current) return; |
| 463 |
|
| 464 |
const observer = new IntersectionObserver(([e])=> { |
| 465 |
if(e.isIntersecting){ |
| 466 |
queryItems({include: mu ? value : [value]}); |
| 467 |
observer.disconnect(); |
| 468 |
} |
| 469 |
}, {threshold: 0.1}); |
| 470 |
|
| 471 |
observer.observe(ref.current); |
| 472 |
|
| 473 |
return ()=> observer.disconnect(); |
| 474 |
}, [value]); |
| 475 |
|
| 476 |
return el('Flex', { |
| 477 |
ref, |
| 478 |
direction: 'column', |
| 479 |
className: 'mu-text', |
| 480 |
gap: 2 |
| 481 |
}, el('ComboboxControl', { |
| 482 |
options, |
| 483 |
value: mu ? comboValue : value, |
| 484 |
placeholder: field.placeholder, |
| 485 |
__next40pxDefaultSize: true, |
| 486 |
onChange: (val)=> { |
| 487 |
if(mu){ |
| 488 |
setComboValue(''); |
| 489 |
|
| 490 |
val && !mu.hasItem(val) && mu.addItem(val); |
| 491 |
|
| 492 |
queryItems({exclude: [...value, val]}); |
| 493 |
}else{ |
| 494 |
update(val); |
| 495 |
} |
| 496 |
}, |
| 497 |
onFilterValueChange: (search)=> queryItems({search: search || '', exclude: mu ? value : null}) |
| 498 |
}), mu && value.length > 0 && value.map((v, i) => el('Flex', { |
| 499 |
...mu.dragProps, |
| 500 |
'data-index': i, |
| 501 |
key: v, |
| 502 |
gap: 0, |
| 503 |
className: 'mu-item', |
| 504 |
}, TruncateText(getLabel(v)), el('Button', { |
| 505 |
icon: 'no-alt', |
| 506 |
size: 'small', |
| 507 |
onClick: ()=> mu.removeItem(i) |
| 508 |
})))); |
| 509 |
}; |
| 510 |
|
| 511 |
const renderFieldset = (field, {index, ...args})=> { |
| 512 |
const mu = field.mu; |
| 513 |
|
| 514 |
if(mu && _.isUndefined(index)){ |
| 515 |
return el('Flex', { |
| 516 |
direction: 'column', |
| 517 |
gap: 2 |
| 518 |
}, field.value.map((item, index)=> renderFieldset(field, { |
| 519 |
index, |
| 520 |
...args |
| 521 |
})), el('FlexItem', null, el('Button', { |
| 522 |
variant: 'secondary', |
| 523 |
onClick: ()=> mu.addItem({}) |
| 524 |
}, field.button_text))); |
| 525 |
} |
| 526 |
|
| 527 |
const names = parseName(field.name); |
| 528 |
const prefix = names[0]+(mu ? '['+index+']' : '')+names.slice(1).map(n => '['+n+']').join(''); |
| 529 |
|
| 530 |
return el('fieldset', { |
| 531 |
...(mu ? { |
| 532 |
...mu.dragProps, |
| 533 |
className: 'mu-item', |
| 534 |
'data-index': index |
| 535 |
} : {}), |
| 536 |
key: index |
| 537 |
}, el('legend', { |
| 538 |
className: 'screen-reader-text' |
| 539 |
}, field.label+(mu ? ' ' + (index + 1) : '')), el('Flex', { |
| 540 |
direction: 'column', |
| 541 |
gap: 2 |
| 542 |
}, field.fields.map(sub => el(Field, { |
| 543 |
key: sub.name, |
| 544 |
field: { |
| 545 |
...sub, |
| 546 |
name: field.fieldset === 'object' ? prefix + parseName(sub.name).map(n => '['+n+']').join('') : sub.name |
| 547 |
}, |
| 548 |
args |
| 549 |
})), mu ? el('Flex', { |
| 550 |
justify: 'flex-start', |
| 551 |
gap: 1 |
| 552 |
}, el('Button', { |
| 553 |
variant: 'secondary', |
| 554 |
isDestructive: true, |
| 555 |
onClick: ()=> mu.removeItem(index) |
| 556 |
}, '删除'), el('Button', { |
| 557 |
icon: 'menu', |
| 558 |
size: 'small', |
| 559 |
className: 'move-item' |
| 560 |
})) : '')); |
| 561 |
}; |
| 562 |
|
| 563 |
const Field = ({field, args})=> { |
| 564 |
const { name, component, multiple, show_if } = field; |
| 565 |
const shouldHide = show_if && !wpjam.compare(getValue(show_if.key, args), show_if.compare, show_if.value); |
| 566 |
|
| 567 |
wp.element.useEffect(()=> { |
| 568 |
if(shouldHide){ |
| 569 |
args.callback(name, null); |
| 570 |
} |
| 571 |
}, [shouldHide]); |
| 572 |
|
| 573 |
if(shouldHide){ |
| 574 |
return null; |
| 575 |
} |
| 576 |
|
| 577 |
let value = getValue(name, args); |
| 578 |
|
| 579 |
if(multiple){ |
| 580 |
value = _.isArray(value) ? value : []; |
| 581 |
|
| 582 |
field.mu = { |
| 583 |
addItem: (...items)=> field.update([...field.value, ...items]), |
| 584 |
updateItem: (index, item)=> field.update(field.value.with(index, item)), |
| 585 |
removeItem: (index)=> field.update(field.value.filter((_, i)=> i !== index)), |
| 586 |
hasItem: (item)=> field.value.some(v => isEqual(v, item)), |
| 587 |
dragProps: { |
| 588 |
draggable: true, |
| 589 |
onMouseDown: (e)=> { |
| 590 |
if(e.currentTarget.querySelector('.move-item')){ |
| 591 |
e.currentTarget._canDrag = !!e.target.closest('.move-item'); |
| 592 |
} |
| 593 |
}, |
| 594 |
onDragStart: (e)=> { |
| 595 |
const t = e.currentTarget; |
| 596 |
|
| 597 |
if(t._canDrag === false){ |
| 598 |
e.preventDefault(); |
| 599 |
}else{ |
| 600 |
t.classList.add('drag-from'); |
| 601 |
e.dataTransfer.effectAllowed = 'move'; |
| 602 |
} |
| 603 |
|
| 604 |
delete t._canDrag; |
| 605 |
}, |
| 606 |
onDragOver: (e)=> { |
| 607 |
e.preventDefault(); |
| 608 |
|
| 609 |
e.currentTarget.parentElement?.querySelectorAll('.drag-to').forEach(el => el.classList.remove('drag-to')); |
| 610 |
|
| 611 |
e.currentTarget.classList.add('drag-to'); |
| 612 |
}, |
| 613 |
onDragEnd: (e)=> { |
| 614 |
const t = e.currentTarget; |
| 615 |
const from = Number(t.dataset.index); |
| 616 |
const toEl = t.parentElement?.querySelector('.drag-to'); |
| 617 |
const to = toEl ? Number(toEl.dataset.index) : null; |
| 618 |
|
| 619 |
t.parentElement?.querySelectorAll('.drag-from, .drag-to').forEach(el => el.classList.remove('drag-from', 'drag-to')); |
| 620 |
|
| 621 |
to != null && from !== to && field.update(field.value.toSpliced(from, 1).toSpliced(to, 0, field.value[from])); |
| 622 |
} |
| 623 |
} |
| 624 |
} |
| 625 |
} |
| 626 |
|
| 627 |
const className = `components-wpjam-`+(multiple ? 'mu-' : '')+`${component.toLowerCase()}-control`; |
| 628 |
field.value = value; |
| 629 |
field.update = (val)=> { |
| 630 |
if(val !== null){ |
| 631 |
val = sanitizeValue(field.schema, val); |
| 632 |
|
| 633 |
const err = validateValue(field.schema, val, field.value); |
| 634 |
|
| 635 |
if(err){ |
| 636 |
return alert(err); |
| 637 |
} |
| 638 |
} |
| 639 |
|
| 640 |
args.callback(name, val); |
| 641 |
}; |
| 642 |
|
| 643 |
let options = field.options; |
| 644 |
|
| 645 |
if(options){ |
| 646 |
options = _.reduce(options, (acc, opt)=> { |
| 647 |
const { data, ...props } = opt; |
| 648 |
const show_if = data?.show_if; |
| 649 |
|
| 650 |
if(!show_if || wpjam.compare(getValue(show_if.key, args), show_if.compare, show_if.value)){ |
| 651 |
acc.push({ ...props, ...(data && data) }); |
| 652 |
} |
| 653 |
|
| 654 |
return acc; |
| 655 |
}, []); |
| 656 |
} |
| 657 |
|
| 658 |
let Control = ''; |
| 659 |
|
| 660 |
if(component == 'Radio'){ |
| 661 |
if(options?.some(o => o.image)){ |
| 662 |
Control = ImageRadioControl; |
| 663 |
} |
| 664 |
}else if(component == 'Select'){ |
| 665 |
if(multiple){ |
| 666 |
Control = MuSelectControl; |
| 667 |
} |
| 668 |
}else{ |
| 669 |
Control = { |
| 670 |
File: FileControl, |
| 671 |
Media: MediaControl, |
| 672 |
Color: ColorControl, |
| 673 |
Checkbox: CheckboxControl, |
| 674 |
Combobox: ComboboxControl, |
| 675 |
Uploader: UploaderControl |
| 676 |
}[component]; |
| 677 |
} |
| 678 |
|
| 679 |
if(Control || component === 'Fieldset'){ |
| 680 |
return el('BaseControl', { |
| 681 |
className, |
| 682 |
..._.pick(field, ['key', 'label', 'help']) |
| 683 |
}, component === 'Fieldset' ? renderFieldset(field, args) : el(Control, { field, ...args })); |
| 684 |
} |
| 685 |
|
| 686 |
if(multiple){ |
| 687 |
Control = 'FormTokenField'; |
| 688 |
|
| 689 |
field.__experimentalShowHowTo = false |
| 690 |
}else{ |
| 691 |
Control = (component == 'Timestamp' ? 'Text' : component)+'Control'; |
| 692 |
|
| 693 |
if(component == 'Timestamp'){ |
| 694 |
if(value && /^\d+$/.test(String(value))){ |
| 695 |
const p2 = (n)=> String(n).padStart(2, '0'); |
| 696 |
const date = new Date(Number(value) * 1000); |
| 697 |
|
| 698 |
value = date.getFullYear()+'-'+p2(date.getMonth() + 1)+'-'+p2(date.getDate())+'T'+p2(date.getHours())+':'+p2(date.getMinutes()); |
| 699 |
} |
| 700 |
|
| 701 |
field.type = 'datetime-local'; |
| 702 |
} |
| 703 |
} |
| 704 |
|
| 705 |
const supportsSize = ['Text', 'Select', 'Textarea', 'Range', 'Timestamp'].includes(component); |
| 706 |
|
| 707 |
return el(Control, { |
| 708 |
...(supportsSize ? { __next40pxDefaultSize: true } : {}), |
| 709 |
className, |
| 710 |
..._.omit(field, ['component', 'show_if', 'schema', 'multiple', 'value', 'update', 'mu']), |
| 711 |
...(options ? { options } : {}), |
| 712 |
...(component === 'Toggle' ? { checked: !!value } : (component === 'Radio' ? { selected: value } : { value: value ?? '' })), |
| 713 |
onChange: (val)=> field.update(val) |
| 714 |
}); |
| 715 |
}; |
| 716 |
|
| 717 |
const Modal = ({panel, data, setOpen})=> { |
| 718 |
const pick = (data, init = {})=> panel.fields.reduce((acc, field)=> ({...acc, ...setValue(field.name, getValue(field.name, {data}), {data: acc})}), {...init}); |
| 719 |
|
| 720 |
const [draft, setDraft] = wp.element.useState(()=> pick(data)); |
| 721 |
const { editPost: edit, savePost: save } = wp.data.useDispatch('core/editor'); |
| 722 |
|
| 723 |
return el('Modal', { |
| 724 |
title: panel.title, |
| 725 |
size: 'medium', |
| 726 |
onRequestClose: ()=> setOpen(false) |
| 727 |
}, el('Flex', { |
| 728 |
direction: 'column', |
| 729 |
gap: 4, |
| 730 |
}, panel.fields.map(field => el(Field, { |
| 731 |
key: field.name, |
| 732 |
field, |
| 733 |
args: { |
| 734 |
data: draft, |
| 735 |
callback: (name, val)=> setDraft(prev => ({ ...prev, ...setValue(name, val, {data: prev})})) |
| 736 |
} |
| 737 |
})), el('Flex', { |
| 738 |
justify: 'flex-end' |
| 739 |
}, el('Button', { |
| 740 |
variant: 'primary', |
| 741 |
onClick: ()=> { |
| 742 |
edit({meta: pick(draft, data)}); |
| 743 |
save(); |
| 744 |
} |
| 745 |
}, '提交')))); |
| 746 |
}; |
| 747 |
|
| 748 |
const Panel = ({panel})=> { |
| 749 |
const data = wp.data.useSelect(select => select('core/editor').getEditedPostAttribute('meta') || {}); |
| 750 |
const edit = wp.data.useDispatch('core/editor').editPost; |
| 751 |
const modal = panel.modal; |
| 752 |
const title = modal ? el('Flex', { |
| 753 |
onClick: (e)=> { |
| 754 |
e.stopPropagation(); |
| 755 |
setOpen(true); |
| 756 |
} |
| 757 |
}, panel.title, el('span', { className: 'dashicons dashicons-admin-generic' })) : panel.title; |
| 758 |
|
| 759 |
const [isOpen, setOpen] = wp.element.useState(false); |
| 760 |
|
| 761 |
return el(wp.element.Fragment, null, el(wp.editor.PluginDocumentSettingPanel, { |
| 762 |
name: panel.name, |
| 763 |
title |
| 764 |
}, modal ? panel.description : el('Flex', { |
| 765 |
direction: 'column', |
| 766 |
gap: 4, |
| 767 |
}, panel.fields.map(field => el(Field, { |
| 768 |
key: field.name, |
| 769 |
field, |
| 770 |
args: { |
| 771 |
data, |
| 772 |
callback : (name, val)=> edit({ meta: setValue(name, val, {data}) }) |
| 773 |
} |
| 774 |
})))), modal && isOpen && el(Modal, {panel, data, setOpen})); |
| 775 |
}; |
| 776 |
|
| 777 |
if(panels?.length){ |
| 778 |
wp.plugins.registerPlugin('wpjam-panels', { |
| 779 |
render: ()=> el(wp.element.Fragment, null, panels.map(panel => el(Panel, { |
| 780 |
key: panel.name, |
| 781 |
panel |
| 782 |
}))) |
| 783 |
}); |
| 784 |
} |
| 785 |
})(window.wp, window._, window.wpjam_page_setting?.block.panels); |