(function(wp, _, panels) { const el = (tag, ...args)=> { return wp.element.createElement(typeof tag === 'string' && /^[A-Z]/.test(tag) ? wp.components[tag] : tag, ...args); }; const isEqual = (a, b)=> String(a) === String(b); const parseName = (name)=> { const parts = name.includes('[') && !name.startsWith('[') && name.endsWith(']') && name.split(/(\[|\])/).filter(t => t !== ''); if(!parts || parts.length % 3 !== 1){ return [name]; } const names = [parts[0]]; for(let i = 1; i < parts.length; i += 3){ if(parts[i] !== '[' || !parts[i+1] || parts[i+2] !== ']'){ return [name]; } names.push(parts[i+1]); } return names; }; const getValue = (name, {data, value_callback})=> { const names = parseName(name); const val = names.reduce((acc, n)=> acc?.[n], value_callback ? {[names[0]]: value_callback(names[0])} : data); return val !== undefined ? val : null; }; const setValue = (name, value, args)=> { const names = parseName(name); const data = { [names[0]]: getValue(names[0], args) }; names.reduce((acc, n, i)=> { if(i === names.length - 1){ if(value === null && names.length > 1){ delete acc[n]; }else{ acc[n] = value; } }else{ acc[n] = _.isArray(acc[n]) ? [...acc[n]] : (_.isObject(acc[n]) ? {...acc[n]} : {}); } return acc[n]; }, data); return data; }; const sanitizeValue = (schema, value)=> { if(value === ''){ return null; } const type = schema.type; if(type === 'array'){ return value.map(v => sanitizeValue(schema.items, v)).filter(v => v !== null); }else if(['integer', 'number'].includes(type)){ value = type === 'integer' ? parseInt(value, 10) : parseFloat(value); return isNaN(value) ? null : value; }else if(type === 'string'){ return value === null ? '' : String(value); } return value; }; const validateValue = (schema, value, prevValue)=> { if(!schema || !_.isArray(value)){ return null; } if(schema.maxItems && value.length > schema.maxItems && (!prevValue || value.length > prevValue.length)){ return '最多支持'+schema.maxItems+'个'; } if(schema.minItems && value.length < schema.minItems && prevValue && value.length < prevValue.length){ return '至少需要'+schema.minItems+'个'; } if(schema.uniqueItems && _.uniq(value.map(String)).length !== value.length){ return '不允许重复'; } return null; }; const TruncateText = (text)=> el('span', {className: 'truncate-text'}, text); const MediaButton = ({field, render})=> { const isId = field.item_type === 'id'; const allowed = field.item_type ? { allowedTypes: [isId ? 'image' : field.item_type] } : {}; return el(wp.blockEditor.MediaUpload, { ...allowed, multiple: !!field.mu, onSelect: (media)=> { const toVal = (m)=> isId ? m.id : m.url; field.mu ? field.mu.addItem(...media.map(toVal)) : field.update(toVal(media)); }, render: render || (({open})=> el('Button', { variant: 'secondary', onClick: open }, field.button_text)) }); }; const FileControl = ({field})=> { const {value, update, mu} = field; const [newVal, setNewVal] = wp.element.useState(''); const length = value.length; const textInput = (val, index)=> el('TextControl', { value: val, placeholder: field.placeholder || 'https://...', __next40pxDefaultSize: true, ...(mu ? { onChange: (v)=> { if(index === length){ setNewVal(v); }else{ mu.updateItem(index, v); } }, onBlur: ()=> { if(index === length){ if(val){ mu.addItem(val); setNewVal(''); } }else if(!val){ mu.removeItem(index); } } } : { onChange: (v)=> update(v || null) }) }); if(mu){ return el('Flex', { direction: 'column', align: 'flex-start', gap: 2 }, [...value, newVal].map((val, index) => el('Flex', { ...(index < length ? {...mu.dragProps, 'data-index': index} : {}), className: 'mu-item', key: index, gap: 1 }, el('FlexBlock', null, textInput(val, index)), index < length ? el('Button', { variant: 'secondary', isDestructive: true, onClick: ()=> mu.removeItem(index) }, '删除') : null, index < length ? el('Button', { icon: 'menu', size: 'small', className: 'move-item' }) : el(MediaButton, { field })))); } return el('Flex', { justify: 'flex-start', gap: 1 }, el('FlexBlock', null, textInput(value)), el(MediaButton, { field })); }; const MediaControl = ({field})=> { const [urlInput, setUrlInput] = wp.element.useState(null); const value = field.value; const isId = field.item_type === 'id'; const mu = field.mu; let urls = mu ? value : (value ? [value] : []); const ids = isId ? urls : []; const cache = wp.element.useRef({}); const data = wp.data.useSelect(select => { const missing = ids.filter(id => !cache.current[id]); return missing.length ? select('core').getEntityRecords('postType', 'attachment', { include: missing, per_page: missing.length }) || [] : []; }, [JSON.stringify(ids)]) if(isId){ data.forEach(m => cache.current[m.id] = m.source_url); urls = ids.map(id => cache.current[id]).filter(Boolean); } if(urlInput !== null){ return el('Flex', { direction: 'column' }, el('TextControl', { value: urlInput, placeholder: 'https://...', onChange: (val)=> setUrlInput(val) }), el('Flex', { justify: 'flex-start' }, el('Button', { variant: 'primary', onClick: ()=> { urlInput && (mu ? mu.addItem(urlInput) : field.update(urlInput)); setUrlInput(null); } }, '应用'), el('Button', { variant: 'secondary', onClick: ()=> setUrlInput(null) }, '取消'))); } if(mu){ return el('Flex', { className: 'mu-img', justify: 'flex-start', wrap: true, gap: 2 }, urls.map((src, i) => { return el('div', { ...mu.dragProps, 'data-index': i, key: i, className: 'mu-item' }, el('img', { src }), el('Button', { icon: 'no-alt', size: 'small', className: 'del-img', onClick: ()=> mu.removeItem(i) })); }), el(MediaButton, { field, render: ({open})=> el('div', { className: 'new-item', onClick: open }, (isId ? '' : el('Button', { icon: 'admin-links', size: 'small', onClick: (e)=> { e.stopPropagation(); setUrlInput(''); } }))) })); } if(urls[0]){ return el('div', { className: 'wpjam-img' }, el(MediaButton, { field, render: ({open})=> el('img', { src: urls[0], onClick: open }) }), el('Button', { icon: 'no-alt', className: 'del-img', onClick: ()=> field.update(null) })); } return el('Flex', { justify: 'flex-start' }, el(MediaButton, { field, render: ({open})=> el('Button', { variant: 'secondary', icon: 'camera', text: field.button_text, onClick: open }) }), (isId ? '' : el('Button', { variant: 'secondary', text: '输入外链', onClick: ()=> setUrlInput(value || '') }))); }; const UploaderControl = ({field})=> { const [uploading, setUploading] = wp.element.useState(false); return el('Flex', { justify: 'flex-start', gap: 1 }, el('FormFileUpload', { accept: field.accept, disabled: uploading, variant: 'secondary', onChange: (e)=> { const file = e.target.files[0]; if(!file) return; setUploading(true); const reader = new FileReader(); reader.onerror = ()=> { setUploading(false); alert('文件读取失败'); }; reader.onload = ()=> { wpjam.post({ action: 'wpjam-upload', name: field.name, filename: file.name, bits: reader.result, _ajax_nonce: field.nonce }).then(data => { setUploading(false); if(data.errcode === 0){ field.update(data.url); }else{ alert(data.errmsg || '上传失败'); } }); }; reader.readAsDataURL(file); } }, field.button_text), (field.value ? el('FlexBlock', null, el('Flex', { justify: 'flex-start', wrap: false, gap: 0 }, el('Button', { icon: 'dismiss', size: 'small', onClick: ()=> field.update(null) }), TruncateText(field.value.split('/').pop()))) : '')); }; const ColorControl = ({field})=> { let value = field.value || '#000000'; return el('Dropdown', { renderToggle: ({isOpen, onToggle})=> el('Button', { onClick: onToggle, style: { color: value, border: `1px solid ${value}` } }, el('ColorIndicator', { colorValue: value }), value || field.button_text || wp.i18n.__('Select Color', 'default')), renderContent: ()=> el('ColorPicker', { color: value, enableAlpha: field.alpha || false, onChange: (color)=> field.update(color) }) }); }; const MuSelectControl = ({field})=> { return el('Dropdown', { focusOnMount: false, popoverProps: { className: 'components-wpjam-select-popover' }, renderToggle: ({isOpen, onToggle})=> el('Button', { icon: 'arrow-down-alt2', iconPosition: 'right', iconSize: 12, className: 'truncate-text', onClick: onToggle, }, field.value.length > 0 ? field.value.map(v => (field.options.find(o => isEqual(o.value, v)) || {}).label || v).join(', ') : (field.placeholder || '请选择')), renderContent: ()=> el(CheckboxControl, {field}) }); }; const ImageRadioControl = ({field})=> { return el('Flex', { justify: 'flex-start', wrap: true, gap: 4, }, field.options.map(opt => { return el('label', { key: opt.value, className: 'image-radio' }, el('input', { type: 'radio', name: field.name, value: opt.value, checked: isEqual(field.value, opt.value), onChange: ()=> field.update(opt.value) }), [].concat(opt.image).slice(0, 2).map((src, i) => el('img', { key: i, src, alt: opt.label })), opt.label); })); }; const CheckboxControl = ({field})=> { return el('Flex', { direction: 'column', gap: 3, }, field.options.map(opt => el('CheckboxControl', { key: opt.value, label: opt.label, checked: field.value.some(v => isEqual(v, opt.value)), onChange: (checked)=> field.update(checked ? [...field.value, opt.value] : field.value.filter(v=> !isEqual(v, opt.value))) }))); }; const ComboboxControl = ({field})=> { const [options, setOptions] = wp.element.useState([]); const [comboValue, setComboValue] = wp.element.useState(''); const labels = wp.element.useRef({}); const ref = wp.element.useRef(null); const getLabel = (v)=> labels.current[String(v)] || String(v); const queryItems = ({search, include, exclude})=> { const query_args = field.query_args || {}; if(search){ query_args[field.data_type === 'post_type' ? 's' : 'search'] = search; } if(exclude?.length){ query_args.exclude = exclude; } wpjam.post({ action: 'wpjam-query', data_type: field.data_type, query_args: query_args, ...(include ? {include} : {}) }).then(data => { if(data.errcode === 0){ data.items.forEach(o => labels.current[String(o.value)] = o.label); setOptions(data.items); } }); }; let {value, mu, update} = field; if(!mu && value !== null){ value = String(value); } wp.element.useEffect(()=> { if(!value || !ref.current) return; const observer = new IntersectionObserver(([e])=> { if(e.isIntersecting){ queryItems({include: mu ? value : [value]}); observer.disconnect(); } }, {threshold: 0.1}); observer.observe(ref.current); return ()=> observer.disconnect(); }, [value]); return el('Flex', { ref, direction: 'column', className: 'mu-text', gap: 2 }, el('ComboboxControl', { options, value: mu ? comboValue : value, placeholder: field.placeholder, __next40pxDefaultSize: true, onChange: (val)=> { if(mu){ setComboValue(''); val && !mu.hasItem(val) && mu.addItem(val); queryItems({exclude: [...value, val]}); }else{ update(val); } }, onFilterValueChange: (search)=> queryItems({search: search || '', exclude: mu ? value : null}) }), mu && value.length > 0 && value.map((v, i) => el('Flex', { ...mu.dragProps, 'data-index': i, key: v, gap: 0, className: 'mu-item', }, TruncateText(getLabel(v)), el('Button', { icon: 'no-alt', size: 'small', onClick: ()=> mu.removeItem(i) })))); }; const renderFieldset = (field, {index, ...args})=> { const mu = field.mu; if(mu && _.isUndefined(index)){ return el('Flex', { direction: 'column', gap: 2 }, field.value.map((item, index)=> renderFieldset(field, { index, ...args })), el('FlexItem', null, el('Button', { variant: 'secondary', onClick: ()=> mu.addItem({}) }, field.button_text))); } const names = parseName(field.name); const prefix = names[0]+(mu ? '['+index+']' : '')+names.slice(1).map(n => '['+n+']').join(''); return el('fieldset', { ...(mu ? { ...mu.dragProps, className: 'mu-item', 'data-index': index } : {}), key: index }, el('legend', { className: 'screen-reader-text' }, field.label+(mu ? ' ' + (index + 1) : '')), el('Flex', { direction: 'column', gap: 2 }, field.fields.map(sub => el(Field, { key: sub.name, field: { ...sub, name: field.fieldset === 'object' ? prefix + parseName(sub.name).map(n => '['+n+']').join('') : sub.name }, args })), mu ? el('Flex', { justify: 'flex-start', gap: 1 }, el('Button', { variant: 'secondary', isDestructive: true, onClick: ()=> mu.removeItem(index) }, '删除'), el('Button', { icon: 'menu', size: 'small', className: 'move-item' })) : '')); }; const Field = ({field, args})=> { const { name, component, multiple, show_if } = field; const shouldHide = show_if && !wpjam.compare(getValue(show_if.key, args), show_if.compare, show_if.value); wp.element.useEffect(()=> { if(shouldHide){ args.callback(name, null); } }, [shouldHide]); if(shouldHide){ return null; } let value = getValue(name, args); if(multiple){ value = _.isArray(value) ? value : []; field.mu = { addItem: (...items)=> field.update([...field.value, ...items]), updateItem: (index, item)=> field.update(field.value.with(index, item)), removeItem: (index)=> field.update(field.value.filter((_, i)=> i !== index)), hasItem: (item)=> field.value.some(v => isEqual(v, item)), dragProps: { draggable: true, onMouseDown: (e)=> { if(e.currentTarget.querySelector('.move-item')){ e.currentTarget._canDrag = !!e.target.closest('.move-item'); } }, onDragStart: (e)=> { const t = e.currentTarget; if(t._canDrag === false){ e.preventDefault(); }else{ t.classList.add('drag-from'); e.dataTransfer.effectAllowed = 'move'; } delete t._canDrag; }, onDragOver: (e)=> { e.preventDefault(); e.currentTarget.parentElement?.querySelectorAll('.drag-to').forEach(el => el.classList.remove('drag-to')); e.currentTarget.classList.add('drag-to'); }, onDragEnd: (e)=> { const t = e.currentTarget; const from = Number(t.dataset.index); const toEl = t.parentElement?.querySelector('.drag-to'); const to = toEl ? Number(toEl.dataset.index) : null; t.parentElement?.querySelectorAll('.drag-from, .drag-to').forEach(el => el.classList.remove('drag-from', 'drag-to')); to != null && from !== to && field.update(field.value.toSpliced(from, 1).toSpliced(to, 0, field.value[from])); } } } } const className = `components-wpjam-`+(multiple ? 'mu-' : '')+`${component.toLowerCase()}-control`; field.value = value; field.update = (val)=> { if(val !== null){ val = sanitizeValue(field.schema, val); const err = validateValue(field.schema, val, field.value); if(err){ return alert(err); } } args.callback(name, val); }; let options = field.options; if(options){ options = _.reduce(options, (acc, opt)=> { const { data, ...props } = opt; const show_if = data?.show_if; if(!show_if || wpjam.compare(getValue(show_if.key, args), show_if.compare, show_if.value)){ acc.push({ ...props, ...(data && data) }); } return acc; }, []); } let Control = ''; if(component == 'Radio'){ if(options?.some(o => o.image)){ Control = ImageRadioControl; } }else if(component == 'Select'){ if(multiple){ Control = MuSelectControl; } }else{ Control = { File: FileControl, Media: MediaControl, Color: ColorControl, Checkbox: CheckboxControl, Combobox: ComboboxControl, Uploader: UploaderControl }[component]; } if(Control || component === 'Fieldset'){ return el('BaseControl', { className, ..._.pick(field, ['key', 'label', 'help']) }, component === 'Fieldset' ? renderFieldset(field, args) : el(Control, { field, ...args })); } if(multiple){ Control = 'FormTokenField'; field.__experimentalShowHowTo = false }else{ Control = (component == 'Timestamp' ? 'Text' : component)+'Control'; if(component == 'Timestamp'){ if(value && /^\d+$/.test(String(value))){ const p2 = (n)=> String(n).padStart(2, '0'); const date = new Date(Number(value) * 1000); value = date.getFullYear()+'-'+p2(date.getMonth() + 1)+'-'+p2(date.getDate())+'T'+p2(date.getHours())+':'+p2(date.getMinutes()); } field.type = 'datetime-local'; } } const supportsSize = ['Text', 'Select', 'Textarea', 'Range', 'Timestamp'].includes(component); return el(Control, { ...(supportsSize ? { __next40pxDefaultSize: true } : {}), className, ..._.omit(field, ['component', 'show_if', 'schema', 'multiple', 'value', 'update', 'mu']), ...(options ? { options } : {}), ...(component === 'Toggle' ? { checked: !!value } : (component === 'Radio' ? { selected: value } : { value: value ?? '' })), onChange: (val)=> field.update(val) }); }; const Modal = ({panel, data, setOpen})=> { const pick = (data, init = {})=> panel.fields.reduce((acc, field)=> ({...acc, ...setValue(field.name, getValue(field.name, {data}), {data: acc})}), {...init}); const [draft, setDraft] = wp.element.useState(()=> pick(data)); const { editPost: edit, savePost: save } = wp.data.useDispatch('core/editor'); return el('Modal', { title: panel.title, size: 'medium', onRequestClose: ()=> setOpen(false) }, el('Flex', { direction: 'column', gap: 4, }, panel.fields.map(field => el(Field, { key: field.name, field, args: { data: draft, callback: (name, val)=> setDraft(prev => ({ ...prev, ...setValue(name, val, {data: prev})})) } })), el('Flex', { justify: 'flex-end' }, el('Button', { variant: 'primary', onClick: ()=> { edit({meta: pick(draft, data)}); save(); } }, '提交')))); }; const Panel = ({panel})=> { const data = wp.data.useSelect(select => select('core/editor').getEditedPostAttribute('meta') || {}); const edit = wp.data.useDispatch('core/editor').editPost; const modal = panel.modal; const title = modal ? el('Flex', { onClick: (e)=> { e.stopPropagation(); setOpen(true); } }, panel.title, el('span', { className: 'dashicons dashicons-admin-generic' })) : panel.title; const [isOpen, setOpen] = wp.element.useState(false); return el(wp.element.Fragment, null, el(wp.editor.PluginDocumentSettingPanel, { name: panel.name, title }, modal ? panel.description : el('Flex', { direction: 'column', gap: 4, }, panel.fields.map(field => el(Field, { key: field.name, field, args: { data, callback : (name, val)=> edit({ meta: setValue(name, val, {data}) }) } })))), modal && isOpen && el(Modal, {panel, data, setOpen})); }; if(panels?.length){ wp.plugins.registerPlugin('wpjam-panels', { render: ()=> el(wp.element.Fragment, null, panels.map(panel => el(Panel, { key: panel.name, panel }))) }); } })(window.wp, window._, window.wpjam_page_setting?.block.panels);