| |
| <?php
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if ( ! defined( 'ABSPATH' ) ) exit;
|
|
|
| const MERCHATURA_FIELDS_META_KEY_V4 = '_merchatura_custom_fields_v4';
|
|
|
|
|
|
|
|
|
| add_filter( 'woocommerce_product_data_tabs', function( $tabs ) {
|
| $tabs['merchatura_custom_fields'] = array(
|
| 'label' => __( 'Custom Fields', 'merchatura' ),
|
| 'target' => 'merchatura_custom_fields_panel',
|
| 'class' => array(),
|
| 'priority' => 80,
|
| );
|
| return $tabs;
|
| } );
|
|
|
|
|
|
|
|
|
| add_action( 'woocommerce_product_data_panels', function() {
|
| ?>
|
| <div id="merchatura_custom_fields_panel" class="panel woocommerce_options_panel hidden">
|
| <div class="options_group">
|
| <p style="margin:0 0 12px;">
|
| <strong><?php esc_html_e( 'Custom Fields', 'merchatura' ); ?></strong><br>
|
| <?php esc_html_e( 'Add fields shown on the product page and saved with the order.', 'merchatura' ); ?>
|
| </p>
|
|
|
| <div id="merchatura-cf-accordion" data-initial="[]"></div>
|
|
|
| <p style="margin:12px 0 0;">
|
| <button type="button" class="button button-primary" id="merchatura-cf-add">
|
| <?php esc_html_e( 'Add Field', 'merchatura' ); ?>
|
| </button>
|
| </p>
|
|
|
| <input type="hidden" id="merchatura_cf_json" name="merchatura_cf_json" value="" />
|
| </div>
|
| </div>
|
|
|
| <style>
|
| #merchatura-cf-accordion { margin-top: 10px; }
|
| .merchatura-cf-item { border: 1px solid
|
| .merchatura-cf-head { display:flex; align-items:center; justify-content:space-between; gap:10px; padding: 12px 14px; background:
|
| .merchatura-cf-title { font-weight: 600; }
|
| .merchatura-cf-subtitle { color:
|
| .merchatura-cf-actions { display:flex; gap:10px; align-items:center; }
|
| .merchatura-cf-actions button { border:0; background:transparent; padding:4px; cursor:pointer; }
|
| .merchatura-cf-actions .dashicons { font-size:18px; width:18px; height:18px; }
|
| .merchatura-cf-body { padding: 14px; display:none; }
|
| .merchatura-cf-body.open { display:block; }
|
|
|
| .merchatura-cf-field { margin: 0 0 12px; }
|
| .merchatura-cf-field label { display:block; font-weight:600; margin: 0 0 6px; }
|
| .merchatura-cf-field input[type="text"],
|
| .merchatura-cf-field select,
|
| .merchatura-cf-field textarea { width: 100%; max-width: 100%; }
|
|
|
|
|
|
|
| .mcf-conds { border-top: 1px solid
|
| .mcf-conds h4 { margin: 0 0 8px; font-size: 13px; }
|
| .mcf-cond-row {
|
| display: grid;
|
| grid-template-columns: 110px 1fr 120px 1fr 34px;
|
| gap: 8px;
|
| align-items: center;
|
| margin-bottom: 8px;
|
| }
|
| .mcf-cond-row select, .mcf-cond-row input[type="text"] { width: 100%; }
|
| .mcf-cond-row button { border:0; background:
|
| .mcf-cond-row button:hover { background:
|
| .mcf-cond-add { margin-top: 6px; }
|
| @media (max-width: 1100px){
|
| .mcf-cond-row { grid-template-columns: 1fr; }
|
| .mcf-cond-row button { width: 44px; }
|
| }
|
|
|
|
|
|
|
| float: none !important;
|
| width: auto !important;
|
| clear: both !important;
|
| display: block !important;
|
| }
|
|
|
|
|
| clear: both !important;
|
| float: none !important;
|
| width: 100% !important;
|
| }
|
|
|
|
|
| clear: both !important;
|
| width: 100% !important;
|
| }
|
|
|
|
|
| float: none !important;
|
| display: inline-block !important;
|
| }
|
|
|
|
|
|
|
| width: 100%;
|
| grid-template-columns: 110px minmax(0, 1fr) 120px minmax(0, 1fr) 34px;
|
| }
|
|
|
|
|
| min-width: 0 !important;
|
| }
|
|
|
| </style>
|
| <?php
|
| } );
|
|
|
|
|
|
|
|
|
| function merchatura_v42_admin_attribute_sources( $product_id ) {
|
| $out = array();
|
|
|
| $product = wc_get_product( $product_id );
|
| if ( ! $product ) return $out;
|
|
|
| $attributes = $product->get_attributes();
|
| foreach ( $attributes as $attr ) {
|
| if ( ! is_a( $attr, 'WC_Product_Attribute' ) ) continue;
|
|
|
| $name = $attr->get_name();
|
| $is_tax = $attr->is_taxonomy();
|
|
|
| $label = $is_tax ? wc_attribute_label( $name ) : $name;
|
|
|
| $key = $is_tax ? 'attr_' . wc_sanitize_taxonomy_name( $name ) : 'attr_' . sanitize_key( $name );
|
|
|
|
|
| $opts = array();
|
| if ( $is_tax ) {
|
| $terms = wc_get_product_terms( $product_id, $name, array( 'fields' => 'names' ) );
|
| if ( is_array( $terms ) ) $opts = $terms;
|
| } else {
|
| $raw = $attr->get_options();
|
| foreach ( $raw as $r ) $opts[] = (string) $r;
|
| }
|
|
|
| $out[] = array(
|
| 'id' => $key,
|
| 'label' => 'Attribute: ' . $label,
|
| 'type' => 'select',
|
| 'options' => array_values( array_unique( array_filter( array_map( 'trim', $opts ) ) ) ),
|
| );
|
| }
|
|
|
| return $out;
|
| }
|
|
|
|
|
|
|
|
|
| add_action( 'admin_footer', function() {
|
| $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
|
| if ( ! $screen || $screen->id !== 'product' ) return;
|
|
|
| global $post;
|
| if ( ! $post || $post->post_type !== 'product' ) return;
|
|
|
| $fields = get_post_meta( $post->ID, MERCHATURA_FIELDS_META_KEY_V4, true );
|
| if ( ! is_array( $fields ) ) $fields = array();
|
|
|
| $attr_sources = merchatura_v42_admin_attribute_sources( $post->ID );
|
|
|
| ?>
|
| <script>
|
| (function(){
|
| const acc = document.getElementById('merchatura-cf-accordion');
|
| const addBtn = document.getElementById('merchatura-cf-add');
|
| const hidden = document.getElementById('merchatura_cf_json');
|
| if(!acc || !addBtn || !hidden) return;
|
|
|
| const initial = <?php echo wp_json_encode( $fields ); ?>;
|
| const ATTR_SOURCES = <?php echo wp_json_encode( $attr_sources ); ?>;
|
|
|
| const TYPES = [
|
| {v:'text', t:'Text'},
|
| {v:'textarea', t:'Textarea'},
|
| {v:'email', t:'Email'},
|
| {v:'select', t:'Dropdown'},
|
| {v:'radio', t:'Radio'},
|
| {v:'file', t:'File upload'}
|
| ];
|
|
|
| function typeLabel(v){ const f = TYPES.find(x=>x.v===v); return f ? f.t : v; }
|
| function escapeHtml(s){ return (s||'').toString().replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); }
|
| function escapeAttr(s){ return escapeHtml(s); }
|
|
|
| // Build a map of "sources" available for conditions:
|
| // - attributes
|
| // - all custom fields (by key)
|
| function buildSources(){
|
| const fieldSources = state
|
| .filter(f => (f.key||'').trim() && (f.name||'').trim())
|
| .map(f => ({
|
| id: 'field_' + (f.key||'').trim(),
|
| label: 'Field: ' + (f.name||'').trim(),
|
| type: f.type || 'text',
|
| options: (f.type==='select' || f.type==='radio')
|
| ? (String(f.box3||'').replace(/\r\n/g,"\n").split("\n").map(x=>x.trim()).filter(Boolean))
|
| : []
|
| }));
|
|
|
| // Merge, with attrs first
|
| return [...ATTR_SOURCES, ...fieldSources];
|
| }
|
|
|
| function getSourceById(id){
|
| const srcs = buildSources();
|
| return srcs.find(s => s.id === id) || null;
|
| }
|
|
|
| let state = Array.isArray(initial) ? initial.map(f => ({
|
| type: f.type || 'text',
|
| name: f.name || '',
|
| box3: f.box3 || '',
|
| required: (f.required === 'yes') ? 'yes' : 'no',
|
| info: f.info || '',
|
| key: f.key || '',
|
| conditions: Array.isArray(f.conditions) ? f.conditions.map(c => ({
|
| action: (c.action === 'hide') ? 'hide' : 'show',
|
| source: c.source || '',
|
| op: (c.op === 'is_not' || c.op === 'contains' || c.op === 'has_file' || c.op === 'no_file') ? c.op : 'is',
|
| value: (c.value ?? '').toString()
|
| })) : [],
|
| _open: false
|
| })) : [];
|
|
|
| function serializeOnly(){
|
| hidden.value = JSON.stringify(state.map(f => ({
|
| type: f.type || 'text',
|
| name: (f.name||'').trim(),
|
| box3: (f.box3||'').toString(),
|
| required: (f.required === 'yes') ? 'yes' : 'no',
|
| info: (f.info||'').toString(),
|
| key: (f.key||'').trim(),
|
| conditions: Array.isArray(f.conditions) ? f.conditions.map(c => ({
|
| action: (c.action === 'hide') ? 'hide' : 'show',
|
| source: (c.source||'').toString(),
|
| op: (c.op||'').toString(),
|
| value: (c.value ?? '').toString()
|
| })) : []
|
| })));
|
| }
|
|
|
| function updateHeaderInline(idx){
|
| const item = acc.querySelector(`.merchatura-cf-item[data-idx="${idx}"]`);
|
| if(!item) return;
|
| const titleEl = item.querySelector('.merchatura-cf-title');
|
| const subEl = item.querySelector('.merchatura-cf-subtitle');
|
|
|
| const title = (state[idx].name && state[idx].name.trim()) ? state[idx].name.trim() : `Field ${idx+1}`;
|
| const subtitle = `${typeLabel(state[idx].type)} · Required: ${state[idx].required === 'yes' ? 'Yes' : 'No'}`;
|
|
|
| if(titleEl) titleEl.textContent = title;
|
| if(subEl) subEl.textContent = subtitle;
|
| }
|
|
|
| function renderConditionRow(idx, cIdx, c){
|
| const sources = buildSources();
|
| const sourceOptions = [
|
| `<option value="">Select field or attribute…</option>`,
|
| ...sources.map(s => `<option value="${escapeAttr(s.id)}" ${s.id===c.source?'selected':''}>${escapeHtml(s.label)}</option>`)
|
| ].join('');
|
|
|
| const src = c.source ? getSourceById(c.source) : null;
|
| const isFileSource = src && src.type === 'file';
|
| const hasOptions = src && (src.type === 'select' || src.type === 'radio' || src.id.startsWith('attr_')) && Array.isArray(src.options) && src.options.length;
|
|
|
| let opOptions = '';
|
| if(isFileSource){
|
| opOptions = `
|
| <option value="has_file" ${c.op==='has_file'?'selected':''}>has file uploaded</option>
|
| <option value="no_file" ${c.op==='no_file'?'selected':''}>doesn't have file uploaded</option>
|
| `;
|
| } else {
|
| opOptions = `
|
| <option value="is" ${c.op==='is'?'selected':''}>is</option>
|
| <option value="is_not" ${c.op==='is_not'?'selected':''}>is not</option>
|
| <option value="contains"${c.op==='contains'?'selected':''}>contains</option>
|
| `;
|
| }
|
|
|
| let valueControl = '';
|
| if(isFileSource){
|
| valueControl = `<input type="text" class="mcf-cond-value" value="" placeholder="(not used)" disabled>`;
|
| } else if(hasOptions){
|
| const opts = src.options.map(o => `<option value="${escapeAttr(o)}" ${o===c.value?'selected':''}>${escapeHtml(o)}</option>`).join('');
|
| valueControl = `<select class="mcf-cond-value"><option value="">Select…</option>${opts}</select>`;
|
| } else {
|
| valueControl = `<input type="text" class="mcf-cond-value" value="${escapeAttr(c.value||'')}" placeholder="Value…">`;
|
| }
|
|
|
| return `
|
| <div class="mcf-cond-row" data-cidx="${cIdx}">
|
| <select class="mcf-cond-action">
|
| <option value="show" ${c.action==='show'?'selected':''}>Show</option>
|
| <option value="hide" ${c.action==='hide'?'selected':''}>Hide</option>
|
| </select>
|
|
|
| <select class="mcf-cond-source">${sourceOptions}</select>
|
|
|
| <select class="mcf-cond-op">${opOptions}</select>
|
|
|
| ${valueControl}
|
|
|
| <button type="button" class="mcf-cond-del" title="Remove">✕</button>
|
| </div>
|
| `;
|
| }
|
|
|
| function render(){
|
| serializeOnly();
|
|
|
| acc.innerHTML = state.map((f, idx) => {
|
| const title = (f.name && f.name.trim()) ? f.name.trim() : `Field ${idx+1}`;
|
| const subtitle = `${typeLabel(f.type)} · Required: ${f.required === 'yes' ? 'Yes' : 'No'}`;
|
|
|
| const typeOptions = TYPES.map(t => `<option value="${t.v}" ${t.v===f.type?'selected':''}>${t.t}</option>`).join('');
|
|
|
| let box3Label = 'Placeholder';
|
| let box3Placeholder = 'e.g. John Smith';
|
|
|
| if (f.type === 'select' || f.type === 'radio') {
|
| box3Label = 'Options (one per line)';
|
| box3Placeholder = "Option 1\nOption 2\nOption 3";
|
| }
|
| if (f.type === 'file') {
|
| box3Label = 'Allowed file extensions (optional)';
|
| box3Placeholder = 'pdf, jpg, png (leave blank for any)';
|
| }
|
|
|
| const condHtml = (Array.isArray(f.conditions) && f.conditions.length)
|
| ? f.conditions.map((c, cIdx) => renderConditionRow(idx, cIdx, c)).join('')
|
| : '';
|
|
|
| return `
|
| <div class="merchatura-cf-item" data-idx="${idx}">
|
| <div class="merchatura-cf-head">
|
| <div>
|
| <div class="merchatura-cf-title">${escapeHtml(title)}</div>
|
| <div class="merchatura-cf-subtitle">${escapeHtml(subtitle)}</div>
|
| </div>
|
| <div class="merchatura-cf-actions">
|
| <button type="button" class="merchatura-cf-moveup" title="Move up"><span class="dashicons dashicons-arrow-up-alt2"></span></button>
|
| <button type="button" class="merchatura-cf-movedown" title="Move down"><span class="dashicons dashicons-arrow-down-alt2"></span></button>
|
| <button type="button" class="merchatura-cf-edit" title="Edit"><span class="dashicons dashicons-edit"></span></button>
|
| <button type="button" class="merchatura-cf-copy" title="Copy"><span class="dashicons dashicons-admin-page"></span></button>
|
| <button type="button" class="merchatura-cf-delete" title="Delete"><span class="dashicons dashicons-trash"></span></button>
|
| </div>
|
| </div>
|
|
|
| <div class="merchatura-cf-body ${f._open ? 'open' : ''}">
|
| <div class="merchatura-cf-field">
|
| <label>Field Type</label>
|
| <select class="merchatura-cf-type">${typeOptions}</select>
|
| </div>
|
|
|
| <div class="merchatura-cf-field">
|
| <label>${escapeHtml(f.type === 'file' ? 'Uploader Label' : 'Field Name')}</label>
|
| <input type="text" class="merchatura-cf-name" value="${escapeAttr(f.name||'')}" placeholder="e.g. Name">
|
| </div>
|
|
|
| <div class="merchatura-cf-field">
|
| <label>${escapeHtml(box3Label)}</label>
|
| <textarea class="merchatura-cf-box3" rows="3" placeholder="${escapeAttr(box3Placeholder)}">${escapeHtml(f.box3||'')}</textarea>
|
| </div>
|
|
|
| <div class="merchatura-cf-field">
|
| <label>Info tooltip (optional)</label>
|
| <textarea class="merchatura-cf-info" rows="2" placeholder="Any extra explanation shown to customers">${escapeHtml(f.info||'')}</textarea>
|
| </div>
|
|
|
| <div class="merchatura-cf-field">
|
| <label>Required</label>
|
| <select class="merchatura-cf-required">
|
| <option value="no" ${f.required==='no'?'selected':''}>No</option>
|
| <option value="yes" ${f.required==='yes'?'selected':''}>Yes</option>
|
| </select>
|
| </div>
|
|
|
| <div class="mcf-conds">
|
| <h4>Conditional Rules</h4>
|
| <div class="mcf-conds-list">
|
| ${condHtml || '<div class="mcf-conds-empty" style="color:
|
| </div>
|
| <button type="button" class="button mcf-cond-add">Add rule</button>
|
| <div style="margin-top:6px;color:#666;font-size:12px;">
|
| Rules are checked using AND logic (all must match).
|
| </div>
|
| </div>
|
|
|
| </div>
|
| </div>
|
| `;
|
| }).join('');
|
|
|
| bind();
|
| }
|
|
|
| function bind(){
|
| acc.querySelectorAll('.merchatura-cf-edit').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| const idx = parseInt(btn.closest('.merchatura-cf-item').dataset.idx, 10);
|
| state[idx]._open = !state[idx]._open;
|
| render();
|
| });
|
| });
|
|
|
| acc.querySelectorAll('.merchatura-cf-copy').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| const idx = parseInt(btn.closest('.merchatura-cf-item').dataset.idx, 10);
|
| const clone = JSON.parse(JSON.stringify(state[idx]));
|
| clone._open = false;
|
| clone.key = '';
|
| state.splice(idx+1, 0, clone);
|
| render();
|
| });
|
| });
|
|
|
| acc.querySelectorAll('.merchatura-cf-delete').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| const idx = parseInt(btn.closest('.merchatura-cf-item').dataset.idx, 10);
|
| state.splice(idx, 1);
|
| render();
|
| });
|
| });
|
|
|
| acc.querySelectorAll('.merchatura-cf-moveup').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| const idx = parseInt(btn.closest('.merchatura-cf-item').dataset.idx, 10);
|
| if (idx <= 0) return;
|
| const tmp = state[idx-1];
|
| state[idx-1] = state[idx];
|
| state[idx] = tmp;
|
| render();
|
| });
|
| });
|
|
|
| acc.querySelectorAll('.merchatura-cf-movedown').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| const idx = parseInt(btn.closest('.merchatura-cf-item').dataset.idx, 10);
|
| if (idx >= state.length - 1) return;
|
| const tmp = state[idx+1];
|
| state[idx+1] = state[idx];
|
| state[idx] = tmp;
|
| render();
|
| });
|
| });
|
|
|
|
|
| acc.querySelectorAll('.merchatura-cf-item').forEach(item => {
|
| const idx = parseInt(item.dataset.idx, 10);
|
|
|
| const type = item.querySelector('.merchatura-cf-type');
|
| const name = item.querySelector('.merchatura-cf-name');
|
| const box3 = item.querySelector('.merchatura-cf-box3');
|
| const info = item.querySelector('.merchatura-cf-info');
|
| const req = item.querySelector('.merchatura-cf-required');
|
|
|
| if(type) type.addEventListener('change', () => { state[idx].type = type.value; serializeOnly(); render(); });
|
|
|
| if(req) req.addEventListener('change', () => {
|
| state[idx].required = req.value;
|
| serializeOnly();
|
| updateHeaderInline(idx);
|
| });
|
|
|
| if(name) name.addEventListener('input', () => {
|
| state[idx].name = name.value;
|
| serializeOnly();
|
| updateHeaderInline(idx);
|
| });
|
|
|
| if(box3) box3.addEventListener('input', () => { state[idx].box3 = box3.value; serializeOnly(); });
|
| if(info) info.addEventListener('input', () => { state[idx].info = info.value; serializeOnly(); });
|
|
|
|
|
| const addRule = item.querySelector('.mcf-cond-add');
|
| if(addRule){
|
| addRule.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| state[idx].conditions = Array.isArray(state[idx].conditions) ? state[idx].conditions : [];
|
| state[idx].conditions.push({ action:'show', source:'', op:'is', value:'' });
|
| render();
|
| });
|
| }
|
|
|
|
|
| item.querySelectorAll('.mcf-cond-row').forEach(row => {
|
| const cIdx = parseInt(row.getAttribute('data-cidx'), 10);
|
|
|
| const action = row.querySelector('.mcf-cond-action');
|
| const source = row.querySelector('.mcf-cond-source');
|
| const op = row.querySelector('.mcf-cond-op');
|
| const valEl = row.querySelector('.mcf-cond-value');
|
| const del = row.querySelector('.mcf-cond-del');
|
|
|
| function sync(){
|
| const c = state[idx].conditions[cIdx];
|
| if(!c) return;
|
| if(action) c.action = action.value === 'hide' ? 'hide' : 'show';
|
| if(source) c.source = source.value || '';
|
| if(op) c.op = op.value || 'is';
|
|
|
| if(valEl && !valEl.disabled){
|
| c.value = (valEl.value ?? '').toString();
|
| } else {
|
| c.value = '';
|
| }
|
|
|
| serializeOnly();
|
| }
|
|
|
| if(action) action.addEventListener('change', sync);
|
|
|
| if(source) source.addEventListener('change', function(){
|
|
|
| const c = state[idx].conditions[cIdx];
|
| if(!c) return;
|
| c.source = source.value || '';
|
|
|
| const src = c.source ? getSourceById(c.source) : null;
|
| if(src && src.type === 'file'){
|
| c.op = 'has_file';
|
| c.value = '';
|
| } else {
|
| c.op = 'is';
|
| c.value = '';
|
| }
|
| render();
|
| });
|
|
|
| if(op) op.addEventListener('change', function(){
|
| const c = state[idx].conditions[cIdx];
|
| if(!c) return;
|
| c.op = op.value || 'is';
|
| serializeOnly();
|
| });
|
|
|
| if(valEl) valEl.addEventListener('input', sync);
|
| if(valEl) valEl.addEventListener('change', sync);
|
|
|
| if(del) del.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| state[idx].conditions.splice(cIdx, 1);
|
| render();
|
| });
|
| });
|
| });
|
| }
|
|
|
| addBtn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| state.push({ type:'text', name:'', box3:'', required:'no', info:'', key:'', conditions:[], _open:false });
|
| render();
|
| });
|
|
|
| render();
|
| })();
|
| </script>
|
| <?php
|
| } );
|
|
|
|
|
|
|
|
|
| add_action( 'woocommerce_admin_process_product_object', function( $product ) {
|
| if ( ! isset( $_POST['merchatura_cf_json'] ) ) return;
|
|
|
| $decoded = json_decode( wp_unslash($_POST['merchatura_cf_json']), true );
|
| if ( ! is_array($decoded) ) {
|
| $product->update_meta_data( MERCHATURA_FIELDS_META_KEY_V4, array() );
|
| return;
|
| }
|
|
|
| $allowed = array( 'text','textarea','email','select','radio','file' );
|
| $allowed_ops = array( 'is','is_not','contains','has_file','no_file' );
|
| $allowed_actions = array( 'show','hide' );
|
|
|
| $clean = array();
|
| $seen = array();
|
|
|
| foreach ( $decoded as $f ) {
|
| $type = isset($f['type']) ? sanitize_key($f['type']) : 'text';
|
| if ( ! in_array($type, $allowed, true) ) $type = 'text';
|
|
|
| $name = isset($f['name']) ? sanitize_text_field($f['name']) : '';
|
| if ( $name === '' ) continue;
|
|
|
| $key = isset($f['key']) ? sanitize_key($f['key']) : '';
|
| if ( $key === '' ) $key = sanitize_key( strtolower( preg_replace('/\s+/', '_', $name ) ) );
|
| if ( $key === '' ) continue;
|
|
|
| $base = $key; $i=2;
|
| while ( isset($seen[$key]) ) { $key = $base . '_' . $i; $i++; }
|
| $seen[$key] = true;
|
|
|
| $box3 = isset($f['box3']) ? trim((string)$f['box3']) : '';
|
| $info = isset($f['info']) ? trim((string)$f['info']) : '';
|
| $required = ( isset($f['required']) && $f['required'] === 'yes' ) ? 'yes' : 'no';
|
|
|
| if ( $type === 'select' || $type === 'radio' ) {
|
| $box3 = str_replace( array("\r\n","\r"), "\n", $box3 );
|
| $lines = array_filter( array_map('trim', explode("\n", $box3)) );
|
| $box3 = implode("\n", $lines );
|
| }
|
|
|
| if ( $type === 'file' ) {
|
| $box3 = strtolower($box3);
|
| $box3 = preg_replace('/[^a-z0-9,\s]/', '', $box3);
|
| $box3 = preg_replace('/\s+/', '', $box3);
|
| }
|
|
|
|
|
| $conds_clean = array();
|
| if ( isset($f['conditions']) && is_array($f['conditions']) ) {
|
| foreach ( $f['conditions'] as $c ) {
|
| $action = isset($c['action']) ? sanitize_key($c['action']) : 'show';
|
| if ( ! in_array($action, $allowed_actions, true) ) $action = 'show';
|
|
|
| $source = isset($c['source']) ? sanitize_text_field($c['source']) : '';
|
| $source = trim($source);
|
|
|
| $op = isset($c['op']) ? sanitize_key($c['op']) : 'is';
|
| if ( ! in_array($op, $allowed_ops, true) ) $op = 'is';
|
|
|
| $value = isset($c['value']) ? sanitize_text_field($c['value']) : '';
|
| $value = trim($value);
|
|
|
|
|
| if ( $op === 'has_file' || $op === 'no_file' ) $value = '';
|
|
|
|
|
| if ( $source === '' ) continue;
|
|
|
| $conds_clean[] = array(
|
| 'action' => $action,
|
| 'source' => $source,
|
| 'op' => $op,
|
| 'value' => $value,
|
| );
|
| }
|
| }
|
|
|
| $clean[] = array(
|
| 'type' => $type,
|
| 'name' => $name,
|
| 'box3' => $box3,
|
| 'required' => $required,
|
| 'info' => $info,
|
| 'key' => $key,
|
| 'conditions' => $conds_clean,
|
| );
|
| }
|
|
|
| $product->update_meta_data( MERCHATURA_FIELDS_META_KEY_V4, $clean );
|
| } );
|
|
|
|
|
|
|
|
|
| function merchatura_v41_get_fields( $product_id ) {
|
| $fields = get_post_meta( $product_id, MERCHATURA_FIELDS_META_KEY_V4, true );
|
| return is_array($fields) ? $fields : array();
|
| }
|
|
|
| function merchatura_v42_get_attr_post_value( $product_id, $source_id ) {
|
|
|
|
|
| $product = wc_get_product( $product_id );
|
| if ( ! $product ) return '';
|
|
|
| $attributes = $product->get_attributes();
|
| foreach ( $attributes as $attr ) {
|
| if ( ! is_a( $attr, 'WC_Product_Attribute' ) ) continue;
|
|
|
| $name = $attr->get_name();
|
| $expected = $attr->is_taxonomy()
|
| ? 'attr_' . wc_sanitize_taxonomy_name( $name )
|
| : 'attr_' . sanitize_key( $name );
|
|
|
| if ( $expected !== $source_id ) continue;
|
|
|
|
|
| $posted_name = $attr->is_taxonomy()
|
| ? 'attribute_' . wc_sanitize_taxonomy_name( $name )
|
| : 'attribute_' . sanitize_key( $name );
|
|
|
| $val = isset($_POST[$posted_name]) ? (string) wp_unslash($_POST[$posted_name]) : '';
|
| $val = trim($val);
|
|
|
|
|
|
|
|
|
| if ( $val !== '' && $attr->is_taxonomy() ) {
|
| $term = get_term_by( 'slug', $val, $name );
|
| if ( $term && ! is_wp_error($term) && ! empty($term->name) ) {
|
|
|
|
|
| return (string) $term->name;
|
| }
|
| }
|
|
|
| return $val;
|
| }
|
|
|
| return '';
|
| }
|
|
|
| function merchatura_v42_condition_matches( $product_id, $all_fields, $cond ) {
|
| $source = isset($cond['source']) ? (string) $cond['source'] : '';
|
| $op = isset($cond['op']) ? (string) $cond['op'] : 'is';
|
| $value = isset($cond['value']) ? (string) $cond['value'] : '';
|
|
|
| $source = trim($source);
|
| $value = trim($value);
|
|
|
|
|
|
|
|
|
| if ( strpos($source, 'attr_') === 0 ) {
|
| $left = merchatura_v42_get_attr_post_value( $product_id, $source );
|
| } elseif ( strpos($source, 'field_') === 0 ) {
|
| $key = substr($source, 6);
|
| $field_name = 'merchatura_cf_' . sanitize_key($key);
|
|
|
|
|
| $srcType = 'text';
|
| foreach ( $all_fields as $f ) {
|
| if ( ! empty($f['key']) && $f['key'] === $key ) {
|
| $srcType = $f['type'] ?? 'text';
|
| break;
|
| }
|
| }
|
|
|
| if ( $srcType === 'file' ) {
|
| $raw = isset($_POST[$field_name]) ? (string) wp_unslash($_POST[$field_name]) : '';
|
| $arr = json_decode( $raw, true );
|
| $has = ( is_array($arr) && ! empty($arr) );
|
| if ( $op === 'has_file' ) return $has;
|
| if ( $op === 'no_file' ) return ! $has;
|
|
|
|
|
| $left = $has ? '1' : '';
|
| } else {
|
| $left = isset($_POST[$field_name]) ? (string) wp_unslash($_POST[$field_name]) : '';
|
| $left = trim($left);
|
| }
|
| } else {
|
| return false;
|
| }
|
|
|
|
|
| if ( $op === 'has_file' ) return ( trim($left) !== '' );
|
| if ( $op === 'no_file' ) return ( trim($left) === '' );
|
|
|
| if ( $op === 'contains' ) {
|
| if ( $value === '' ) return false;
|
| return ( stripos( (string) $left, (string) $value ) !== false );
|
| }
|
|
|
| if ( $op === 'is_not' ) {
|
| return ( (string) $left !== (string) $value );
|
| }
|
|
|
|
|
| return ( (string) $left === (string) $value );
|
| }
|
|
|
| function merchatura_v42_field_is_visible_from_post( $product_id, $field, $all_fields ) {
|
| $conds = isset($field['conditions']) && is_array($field['conditions']) ? $field['conditions'] : array();
|
| if ( empty($conds) ) return true;
|
|
|
|
|
| $allMatch = true;
|
| foreach ( $conds as $c ) {
|
| if ( ! merchatura_v42_condition_matches( $product_id, $all_fields, $c ) ) {
|
| $allMatch = false;
|
| break;
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
| $anyHideMatch = false;
|
| $hasShowRule = false;
|
| $allShowMatch = true;
|
|
|
| foreach ( $conds as $c ) {
|
| $action = ($c['action'] ?? 'show') === 'hide' ? 'hide' : 'show';
|
| $match = merchatura_v42_condition_matches( $product_id, $all_fields, $c );
|
|
|
| if ( $action === 'hide' && $match ) $anyHideMatch = true;
|
| if ( $action === 'show' ) {
|
| $hasShowRule = true;
|
| if ( ! $match ) $allShowMatch = false;
|
| }
|
| }
|
|
|
| if ( $anyHideMatch ) return false;
|
| if ( $hasShowRule ) return $allShowMatch;
|
|
|
|
|
| return true;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| add_action( 'wp_footer', function() {
|
| if ( ! is_product() ) return;
|
| global $product;
|
| if ( ! $product ) return;
|
|
|
| $fields = merchatura_v41_get_fields( $product->get_id() );
|
| $has_file = false;
|
| foreach ( $fields as $f ) { if ( ($f['type'] ?? '') === 'file' ) { $has_file = true; break; } }
|
| if ( ! $has_file ) return;
|
|
|
| $nonce = wp_create_nonce( 'merchatura_v41_upload' );
|
| ?>
|
| <script>
|
| (function(){
|
| const ajaxUrl = "<?php echo esc_js( admin_url('admin-ajax.php') ); ?>";
|
| const nonce = "<?php echo esc_js( $nonce ); ?>";
|
|
|
| function qs(sel, root){ return (root||document).querySelector(sel); }
|
| function qsa(sel, root){ return Array.from((root||document).querySelectorAll(sel)); }
|
|
|
| function parseHidden(hidden){
|
| try {
|
| const v = (hidden.value || '').trim();
|
| if(!v) return [];
|
| const arr = JSON.parse(v);
|
| return Array.isArray(arr) ? arr : [];
|
| } catch(e){ return []; }
|
| }
|
|
|
| function writeHidden(hidden, arr){
|
| hidden.value = JSON.stringify(arr || []);
|
| }
|
|
|
| function escapeHtml(s){
|
| return (s||'').toString().replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
| }
|
|
|
| function renderList(box){
|
| const hiddenId = box.getAttribute('data-hidden-id');
|
| const hidden = document.getElementById(hiddenId);
|
| const list = qs('.mcf-upload-list', box);
|
| if(!hidden || !list) return;
|
|
|
| const files = parseHidden(hidden);
|
| list.innerHTML = files.map((f, idx) => {
|
| const safeName = (f.name || 'File').toString();
|
| return `
|
| <div class="mcf-upload-item" data-idx="${idx}">
|
| <a class="mcf-upload-link" href="${f.url}" target="_blank" rel="noopener">${escapeHtml(safeName)}</a>
|
| <button type="button" class="mcf-upload-remove" aria-label="Remove file">Remove</button>
|
| </div>
|
| `;
|
| }).join('');
|
|
|
| list.querySelectorAll('.mcf-upload-remove').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| const row = btn.closest('.mcf-upload-item');
|
| const idx = parseInt(row.dataset.idx, 10);
|
| const files2 = parseHidden(hidden);
|
| files2.splice(idx, 1);
|
| writeHidden(hidden, files2);
|
| renderList(box);
|
|
|
| // trigger conditional refresh
|
| document.dispatchEvent(new CustomEvent('mcf:filesChanged'));
|
| });
|
| });
|
| }
|
|
|
| function setStatus(box, text){
|
| const s = qs('.mcf-upload-status', box);
|
| if(s) s.textContent = text || '';
|
| }
|
|
|
| function setProgress(box, pct){
|
| const bar = qs('.mcf-upload-bar > span', box);
|
| const wrap = qs('.mcf-upload-bar', box);
|
| if(!bar || !wrap) return;
|
| wrap.style.display = 'block';
|
| bar.style.width = Math.max(0, Math.min(100, pct)) + '%';
|
| }
|
|
|
| window.__mcfUploadsInFlight = window.__mcfUploadsInFlight || 0;
|
|
|
| function setCartDisabled(disabled){
|
| const btn = document.querySelector('form.cart button.single_add_to_cart_button');
|
| if(!btn) return;
|
| btn.disabled = !!disabled;
|
| btn.classList.toggle('disabled', !!disabled);
|
| btn.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
| }
|
|
|
| document.addEventListener('submit', function(e){
|
| const form = e.target;
|
| if(!form || !form.matches('form.cart')) return;
|
| if((window.__mcfUploadsInFlight || 0) > 0){
|
| e.preventDefault();
|
| alert('Please wait — your file upload is still finishing.');
|
| }
|
| }, true);
|
|
|
| function uploadOne(box, file){
|
| const hiddenId = box.getAttribute('data-hidden-id');
|
| const hidden = document.getElementById(hiddenId);
|
| if(!hidden) return Promise.reject('Missing hidden field');
|
|
|
| window.__mcfUploadsInFlight++;
|
| setCartDisabled(true);
|
|
|
| setStatus(box, 'Uploading…');
|
| setProgress(box, 5);
|
|
|
| return new Promise((resolve, reject) => {
|
| const form = new FormData();
|
| form.append('action', 'merchatura_v41_upload_file');
|
| form.append('nonce', nonce);
|
| form.append('file', file);
|
|
|
| const xhr = new XMLHttpRequest();
|
| xhr.open('POST', ajaxUrl, true);
|
|
|
| xhr.upload.addEventListener('progress', function(e){
|
| if(!e.lengthComputable) return;
|
| const pct = Math.round((e.loaded / e.total) * 100);
|
| setProgress(box, Math.min(95, pct));
|
| });
|
|
|
| xhr.onreadystatechange = function(){
|
| if(xhr.readyState !== 4) return;
|
|
|
| window.__mcfUploadsInFlight = Math.max(0, (window.__mcfUploadsInFlight || 1) - 1);
|
| if((window.__mcfUploadsInFlight || 0) === 0){
|
| setCartDisabled(false);
|
| }
|
|
|
| if(xhr.status !== 200){
|
| setStatus(box, 'Upload failed.');
|
| setProgress(box, 0);
|
| reject('HTTP ' + xhr.status);
|
| return;
|
| }
|
|
|
| try{
|
| const res = JSON.parse(xhr.responseText);
|
| if(!res || !res.success){
|
| setStatus(box, (res && res.data && res.data.message) ? res.data.message : 'Upload failed.');
|
| setProgress(box, 0);
|
| reject('Server error');
|
| return;
|
| }
|
|
|
| const files = parseHidden(hidden);
|
| files.push({ id: res.data.attachment_id, url: res.data.url, name: res.data.original_name || file.name });
|
| writeHidden(hidden, files);
|
| renderList(box);
|
|
|
| setStatus(box, 'Uploaded: ' + (res.data.original_name || file.name));
|
| setProgress(box, 100);
|
|
|
| // trigger conditional refresh
|
| document.dispatchEvent(new CustomEvent('mcf:filesChanged'));
|
|
|
| resolve(res.data);
|
|
|
| }catch(err){
|
| setStatus(box, 'Upload failed.');
|
| setProgress(box, 0);
|
| reject(err);
|
| }
|
| };
|
|
|
| xhr.send(form);
|
| });
|
| }
|
|
|
| async function uploadManySequential(box, files){
|
| const arr = Array.from(files || []);
|
| if(!arr.length) return;
|
|
|
| for(const f of arr){
|
| try{
|
| setProgress(box, 0);
|
| await uploadOne(box, f);
|
| }catch(e){}
|
| }
|
|
|
| setStatus(box, 'All files uploaded.');
|
| setProgress(box, 100);
|
| }
|
|
|
| function initUploader(box){
|
| const fileInput = qs('input[type="file"]', box);
|
| const drop = qs('.mcf-upload-drop', box);
|
| const hiddenId = box.getAttribute('data-hidden-id');
|
| const hidden = document.getElementById(hiddenId);
|
|
|
| if(!fileInput || !drop || !hidden) return;
|
|
|
| renderList(box);
|
|
|
| function handleFiles(files){
|
| if(!files || !files.length) return;
|
| uploadManySequential(box, files);
|
| }
|
|
|
| fileInput.addEventListener('change', function(){
|
| handleFiles(fileInput.files);
|
| fileInput.value = '';
|
| });
|
|
|
| ['dragenter','dragover'].forEach(evt => {
|
| drop.addEventListener(evt, function(e){
|
| e.preventDefault(); e.stopPropagation();
|
| drop.classList.add('is-dragover');
|
| });
|
| });
|
| ['dragleave','drop'].forEach(evt => {
|
| drop.addEventListener(evt, function(e){
|
| e.preventDefault(); e.stopPropagation();
|
| drop.classList.remove('is-dragover');
|
| });
|
| });
|
| drop.addEventListener('drop', function(e){
|
| const dt = e.dataTransfer;
|
| if(dt && dt.files) handleFiles(dt.files);
|
| });
|
| }
|
|
|
| qsa('.mcf-upload-box').forEach(initUploader);
|
| })();
|
| </script>
|
| <?php
|
| } );
|
|
|
| /* =========================================================
|
| * Front-end: Render fields + attach condition data attributes
|
| * ========================================================= */
|
| add_action( 'woocommerce_before_add_to_cart_button', function() {
|
| global $product;
|
| if ( ! $product ) return;
|
|
|
| $fields = merchatura_v41_get_fields( $product->get_id() );
|
| if ( empty($fields) ) return;
|
|
|
| echo '<div class="merchatura-custom-fields">';
|
|
|
| foreach ( $fields as $f ) {
|
| $type = $f['type'] ?? 'text';
|
| $name = $f['name'] ?? '';
|
| $key = $f['key'] ?? '';
|
| $box3 = $f['box3'] ?? '';
|
| $info = $f['info'] ?? '';
|
| $required = ( (($f['required'] ?? 'no') === 'yes') );
|
|
|
| $conditions = ( isset($f['conditions']) && is_array($f['conditions']) ) ? $f['conditions'] : array();
|
|
|
| if ( ! $name || ! $key ) continue;
|
|
|
| $field_name = 'merchatura_cf_' . $key;
|
| $field_id = 'merchatura_cf_' . $key;
|
|
|
| $conds_attr = ! empty($conditions) ? esc_attr( wp_json_encode( $conditions ) ) : '';
|
|
|
| echo '<div class="mcf-row" data-mcf-key="' . esc_attr($key) . '" data-mcf-type="' . esc_attr($type) . '" data-mcf-required="' . ($required ? '1':'0') . '" ' . ( $conds_attr ? 'data-mcf-conds="'.$conds_attr.'"' : '' ) . '>';
|
|
|
| echo '<label class="mcf-label" for="' . esc_attr($field_id) . '">';
|
| echo esc_html($name);
|
| if ($required) echo ' <span class="mcf-required">*</span>';
|
|
|
| if ( ! empty($info) ) {
|
| echo ' <span class="mcf-tooltip" tabindex="0" aria-label="' . esc_attr($info) . '">';
|
| echo '<span class="mcf-tooltip-icon">i</span>';
|
| echo '<span class="mcf-tooltip-box">' . esc_html($info) . '</span>';
|
| echo '</span>';
|
| }
|
| echo '</label>';
|
|
|
| if ( $type === 'textarea' ) {
|
| echo '<textarea data-mcf-input-key="' . esc_attr($key) . '" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '"' . ( $required ? ' required' : '' )
|
| . ( $box3 ? ' placeholder="' . esc_attr($box3) . '"' : '' ) . '></textarea>';
|
|
|
| } elseif ( $type === 'select' ) {
|
| $options = array_filter( array_map('trim', explode("\n", str_replace(array("\r\n","\r"), "\n", (string)$box3))) );
|
| echo '<select data-mcf-input-key="' . esc_attr($key) . '" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '"' . ( $required ? ' required' : '' ) . '>';
|
| echo '<option value="">' . esc_html__('Select…', 'merchatura') . '</option>';
|
| foreach ($options as $opt) echo '<option value="' . esc_attr($opt) . '">' . esc_html($opt) . '</option>';
|
| echo '</select>';
|
|
|
| } elseif ( $type === 'radio' ) {
|
| $options = array_filter( array_map('trim', explode("\n", str_replace(array("\r\n","\r"), "\n", (string)$box3))) );
|
| echo '<div class="mcf-radio-wrap" id="' . esc_attr($field_id) . '">';
|
| foreach ($options as $i => $opt) {
|
| $rid = $field_id . '_opt_' . $i;
|
| echo '<label class="mcf-radio">';
|
| echo '<input data-mcf-input-key="' . esc_attr($key) . '" type="radio" name="' . esc_attr($field_name) . '" id="' . esc_attr($rid) . '" value="' . esc_attr($opt) . '"' . ( $required ? ' required' : '' ) . '>';
|
| echo '<span>' . esc_html($opt) . '</span>';
|
| echo '</label>';
|
| }
|
| echo '</div>';
|
|
|
| } elseif ( $type === 'file' ) {
|
| $hidden_id = 'mcf_file_hidden_' . $key;
|
| echo '<input data-mcf-input-key="' . esc_attr($key) . '" type="hidden" id="' . esc_attr($hidden_id) . '" name="' . esc_attr($field_name) . '" value="[]">';
|
|
|
| $accept = '';
|
| if ( ! empty($box3) ) {
|
| $parts = array_filter(array_map('trim', explode(',', $box3)));
|
| $parts = array_map(function($p){ return '.' . ltrim($p, '.'); }, $parts);
|
| $accept = implode(',', $parts);
|
| }
|
|
|
| echo '<div class="mcf-upload-box" data-hidden-id="' . esc_attr($hidden_id) . '">';
|
| echo '<div class="mcf-upload-drop">';
|
| echo '<div class="mcf-upload-inner">';
|
| echo '<div class="mcf-upload-icon">☁</div>';
|
| echo '<div class="mcf-upload-text">Drop Files Here, Or</div>';
|
| echo '<label class="mcf-upload-btn">';
|
| echo 'Browse…';
|
| echo '<input type="file" multiple ' . ( $accept ? 'accept="' . esc_attr($accept) . '"' : '' ) . ' style="display:none;">';
|
| echo '</label>';
|
| echo '</div>';
|
| echo '<div class="mcf-upload-status"></div>';
|
| echo '<div class="mcf-upload-bar"><span></span></div>';
|
| echo '</div>';
|
|
|
| echo '<div class="mcf-upload-list"></div>';
|
| echo '</div>';
|
|
|
| } else {
|
| $input_type = ( $type === 'email' ) ? 'email' : 'text';
|
| echo '<input data-mcf-input-key="' . esc_attr($key) . '" type="' . esc_attr($input_type) . '" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '"'
|
| . ( $required ? ' required' : '' )
|
| . ( $box3 ? ' placeholder="' . esc_attr($box3) . '"' : '' )
|
| . '>';
|
| }
|
|
|
| echo '</div>'; // .mcf-row
|
| }
|
|
|
| echo '</div>';
|
| } );
|
|
|
| /* =========================================================
|
| * Front-end: Conditional logic runner (attributes + fields)
|
| * ========================================================= */
|
| add_action( 'wp_footer', function() {
|
| if ( ! is_product() ) return;
|
| ?>
|
| <script>
|
| (function(){
|
| const form = document.querySelector('form.cart');
|
| if(!form) return;
|
|
|
| function parseJSON(s){
|
| try{ return JSON.parse(s); }catch(e){ return null; }
|
| }
|
|
|
| function getFieldValueByKey(key){
|
| // custom field input(s)
|
| const els = form.querySelectorAll(`[data-mcf-input-key="${CSS.escape(key)}"]`);
|
| if(!els || !els.length) return '';
|
|
|
| // file is stored in hidden JSON
|
| const first = els[0];
|
| if(first && first.type === 'hidden'){
|
| const arr = parseJSON(first.value || '[]');
|
| return Array.isArray(arr) && arr.length ? '__HAS_FILE__' : '';
|
| }
|
|
|
| // radio group
|
| if(first && first.type === 'radio'){
|
| const checked = form.querySelector(`[data-mcf-input-key="${CSS.escape(key)}"]:checked`);
|
| return checked ? (checked.value || '') : '';
|
| }
|
|
|
| return (first.value || '').trim();
|
| }
|
|
|
| function getAttributeValueBySourceId(sourceId){
|
| // sourceId looks like attr_pa_color OR attr_size
|
| // find attribute_*
|
| // we'll attempt to match the suffix to the name in the cart form.
|
| // e.g. attr_pa_color -> attribute_pa_color
|
| const name = 'attribute_' + sourceId.replace(/^attr_/, '');
|
| const el = form.querySelector(`[name="${CSS.escape(name)}"]`);
|
| if(!el) return '';
|
| return (el.value || '').trim();
|
| }
|
|
|
| function evalCond(cond){
|
| const action = (cond.action === 'hide') ? 'hide' : 'show';
|
| const source = (cond.source || '').toString();
|
| const op = (cond.op || 'is').toString();
|
| const value = (cond.value || '').toString();
|
|
|
| let left = '';
|
| let isFileSource = false;
|
|
|
| if(source.startsWith('attr_')){
|
| left = getAttributeValueBySourceId(source);
|
| } else if(source.startsWith('field_')){
|
| const key = source.replace(/^field_/, '');
|
| const v = getFieldValueByKey(key);
|
| if(v === '__HAS_FILE__'){ isFileSource = true; left = v; }
|
| else left = v;
|
| } else {
|
| return {action, match:false};
|
| }
|
|
|
| // file operators
|
| if(op === 'has_file'){
|
| return {action, match: (left === '__HAS_FILE__')};
|
| }
|
| if(op === 'no_file'){
|
| return {action, match: (left !== '__HAS_FILE__')};
|
| }
|
|
|
| if(op === 'contains'){
|
| if(!value) return {action, match:false};
|
| return {action, match: (left.toLowerCase().indexOf(value.toLowerCase()) !== -1)};
|
| }
|
|
|
| if(op === 'is_not'){
|
| return {action, match: (left !== value)};
|
| }
|
|
|
| // default is
|
| return {action, match: (left === value)};
|
| }
|
|
|
| function setRowVisible(row, visible){
|
| row.style.display = visible ? '' : 'none';
|
|
|
| // Disable inputs inside hidden rows so they:
|
| // - don't trigger browser required validation
|
| // - don't submit values
|
| const inputs = row.querySelectorAll('input, select, textarea');
|
| inputs.forEach(inp => {
|
| if(!inp.dataset) return;
|
|
|
| // remember original required
|
| if(inp.dataset.mcfWasReq === undefined){
|
| inp.dataset.mcfWasReq = inp.required ? '1' : '0';
|
| }
|
|
|
| if(!visible){
|
| inp.disabled = true;
|
| inp.required = false;
|
| } else {
|
| inp.disabled = false;
|
| inp.required = (inp.dataset.mcfWasReq === '1');
|
| }
|
| });
|
| }
|
|
|
| function applyAll(){
|
| const rows = form.querySelectorAll('.mcf-row[data-mcf-conds]');
|
| rows.forEach(row => {
|
| const condsRaw = row.getAttribute('data-mcf-conds');
|
| const conds = parseJSON(condsRaw);
|
| if(!Array.isArray(conds) || !conds.length){
|
| setRowVisible(row, true);
|
| return;
|
| }
|
|
|
| // Priority:
|
| // - If any HIDE rule matches => hide
|
| // - If there are SHOW rules => show only if all show rules match
|
| let anyHideMatch = false;
|
| let hasShowRule = false;
|
| let allShowMatch = true;
|
|
|
| conds.forEach(c => {
|
| const res = evalCond(c || {});
|
| if(res.action === 'hide' && res.match) anyHideMatch = true;
|
| if(res.action === 'show'){
|
| hasShowRule = true;
|
| if(!res.match) allShowMatch = false;
|
| }
|
| });
|
|
|
| let visible = true;
|
| if(anyHideMatch) visible = false;
|
| else if(hasShowRule) visible = allShowMatch;
|
|
|
| setRowVisible(row, visible);
|
| });
|
| }
|
|
|
| // Run on:
|
| // - any change on the cart form (attributes/fields)
|
| // - upload/remove files events
|
| form.addEventListener('change', applyAll);
|
| form.addEventListener('input', applyAll);
|
| document.addEventListener('mcf:filesChanged', applyAll);
|
|
|
| // run once
|
| applyAll();
|
| })();
|
| </script>
|
| <?php
|
| }, 20 );
|
|
|
| /* =========================================================
|
| * AJAX upload (your existing handler unchanged)
|
| * ========================================================= */
|
| add_action( 'wp_ajax_merchatura_v41_upload_file', 'merchatura_v41_upload_file' );
|
| add_action( 'wp_ajax_nopriv_merchatura_v41_upload_file', 'merchatura_v41_upload_file' );
|
|
|
| function merchatura_v41_upload_file() {
|
| if ( ! isset($_POST['nonce']) || ! wp_verify_nonce( sanitize_text_field($_POST['nonce']), 'merchatura_v41_upload' ) ) {
|
| wp_send_json_error( array( 'message' => 'Invalid security token.' ), 403 );
|
| }
|
|
|
| if ( empty($_FILES['file']) || ! isset($_FILES['file']['tmp_name']) ) {
|
| wp_send_json_error( array( 'message' => 'No file received.' ), 400 );
|
| }
|
|
|
| require_once ABSPATH . 'wp-admin/includes/file.php';
|
| require_once ABSPATH . 'wp-admin/includes/media.php';
|
| require_once ABSPATH . 'wp-admin/includes/image.php';
|
|
|
| $file = $_FILES['file'];
|
|
|
| if ( ! empty($file['size']) && (int)$file['size'] > 20 * 1024 * 1024 ) {
|
| wp_send_json_error( array( 'message' => 'File too large.' ), 400 );
|
| }
|
|
|
| $overrides = array( 'test_form' => false );
|
| $uploaded = wp_handle_upload( $file, $overrides );
|
|
|
| if ( isset($uploaded['error']) ) {
|
| wp_send_json_error( array( 'message' => $uploaded['error'] ), 400 );
|
| }
|
|
|
| $attachment = array(
|
| 'post_mime_type' => $uploaded['type'],
|
| 'post_title' => sanitize_file_name( $file['name'] ),
|
| 'post_content' => '',
|
| 'post_status' => 'inherit'
|
| );
|
|
|
| $attach_id = wp_insert_attachment( $attachment, $uploaded['file'] );
|
| if ( is_wp_error($attach_id) ) {
|
| wp_send_json_error( array( 'message' => 'Could not create attachment.' ), 500 );
|
| }
|
|
|
| $attach_data = wp_generate_attachment_metadata( $attach_id, $uploaded['file'] );
|
| wp_update_attachment_metadata( $attach_id, $attach_data );
|
|
|
| wp_send_json_success( array(
|
| 'attachment_id' => $attach_id,
|
| 'url' => $uploaded['url'],
|
| 'original_name' => $file['name'],
|
| ) );
|
| }
|
|
|
| /* =========================================================
|
| * Validation (required + email + required file: at least 1)
|
| * ✅ Updated: hidden fields are ignored based on conditions
|
| * ========================================================= */
|
| add_filter( 'woocommerce_add_to_cart_validation', function( $passed, $product_id, $quantity ) {
|
| $fields = merchatura_v41_get_fields( $product_id );
|
| if ( empty($fields) ) return $passed;
|
|
|
| foreach ( $fields as $f ) {
|
| $type = $f['type'] ?? 'text';
|
| $name = $f['name'] ?? '';
|
| $key = $f['key'] ?? '';
|
| $required = ( ($f['required'] ?? 'no') === 'yes' );
|
|
|
| if ( ! $name || ! $key ) continue;
|
|
|
| // If field is hidden due to conditional logic, ignore it (including required)
|
| if ( ! merchatura_v42_field_is_visible_from_post( $product_id, $f, $fields ) ) {
|
| continue;
|
| }
|
|
|
| $field_name = 'merchatura_cf_' . $key;
|
| $raw = isset($_POST[$field_name]) ? trim((string) wp_unslash($_POST[$field_name])) : '';
|
|
|
| if ( $type === 'file' ) {
|
| $filesArr = array();
|
| if ($raw !== '') {
|
| $decoded = json_decode($raw, true);
|
| if ( is_array($decoded) ) $filesArr = $decoded;
|
| }
|
|
|
| if ( $required && empty($filesArr) ) {
|
| wc_add_notice( sprintf( __( 'Please upload at least one file for: %s', 'merchatura' ), $name ), 'error' );
|
| return false;
|
| }
|
| continue;
|
| }
|
|
|
| if ( $required && $raw === '' ) {
|
| wc_add_notice( sprintf( __( 'Please complete: %s', 'merchatura' ), $name ), 'error' );
|
| return false;
|
| }
|
|
|
| if ( $raw !== '' && $type === 'email' && ! is_email($raw) ) {
|
| wc_add_notice( sprintf( __( 'Please enter a valid email for: %s', 'merchatura' ), $name ), 'error' );
|
| return false;
|
| }
|
| }
|
|
|
| return $passed;
|
| }, 20, 3 );
|
|
|
| /* =========================================================
|
| * Save into cart item data
|
| * ✅ Updated: hidden fields are not saved
|
| * ========================================================= */
|
| add_filter( 'woocommerce_add_cart_item_data', function( $cart_item_data, $product_id ) {
|
| $fields = merchatura_v41_get_fields( $product_id );
|
| if ( empty($fields) ) return $cart_item_data;
|
|
|
| $values = array();
|
|
|
| foreach ( $fields as $f ) {
|
| $type = $f['type'] ?? 'text';
|
| $name = $f['name'] ?? '';
|
| $key = $f['key'] ?? '';
|
| if ( ! $name || ! $key ) continue;
|
|
|
| // If field hidden, skip saving
|
| if ( ! merchatura_v42_field_is_visible_from_post( $product_id, $f, $fields ) ) {
|
| continue;
|
| }
|
|
|
| $field_name = 'merchatura_cf_' . $key;
|
| if ( ! isset($_POST[$field_name]) ) continue;
|
|
|
| $raw = trim((string) wp_unslash($_POST[$field_name]));
|
| if ( $raw === '' ) continue;
|
|
|
| if ( $type === 'file' ) {
|
| $decoded = json_decode($raw, true);
|
| if ( ! is_array($decoded) || empty($decoded) ) continue;
|
|
|
| $links = array();
|
| foreach ( $decoded as $file ) {
|
| $url = isset($file['url']) ? esc_url_raw($file['url']) : '';
|
| $nm = isset($file['name']) ? sanitize_text_field($file['name']) : 'File';
|
| if ( $url ) $links[] = array('url' => $url, 'name' => $nm);
|
| }
|
| if ( empty($links) ) continue;
|
|
|
| $values[] = array(
|
| 'key' => $key,
|
| 'label' => $name,
|
| 'type' => 'file',
|
| 'value' => $links,
|
| );
|
| } else {
|
| $val = ( $type === 'email' ) ? sanitize_email($raw) : sanitize_text_field($raw);
|
| $values[] = array(
|
| 'key' => $key,
|
| 'label' => $name,
|
| 'type' => $type,
|
| 'value' => $val,
|
| );
|
| }
|
| }
|
|
|
| if ( ! empty($values) ) {
|
| $cart_item_data['merchatura_custom_fields'] = $values;
|
| $cart_item_data['merchatura_custom_fields_key'] = md5( wp_json_encode($values) );
|
| }
|
|
|
| return $cart_item_data;
|
| }, 20, 2 );
|
|
|
| /* =========================================================
|
| * Display in cart + checkout (unchanged)
|
| * ========================================================= */
|
| add_filter( 'woocommerce_get_item_data', function( $item_data, $cart_item ) {
|
| if ( empty($cart_item['merchatura_custom_fields']) || ! is_array($cart_item['merchatura_custom_fields']) ) return $item_data;
|
|
|
| foreach ( $cart_item['merchatura_custom_fields'] as $f ) {
|
| $label = $f['label'] ?? '';
|
| $type = $f['type'] ?? '';
|
| $value = $f['value'] ?? '';
|
|
|
| if ( $type === 'file' && is_array($value) ) {
|
| $links = array();
|
| foreach ($value as $file) {
|
| $url = isset($file['url']) ? $file['url'] : '';
|
| $nm = isset($file['name']) ? $file['name'] : 'File';
|
| if($url) $links[] = '<a href="' . esc_url($url) . '" target="_blank" rel="noopener">' . esc_html($nm) . '</a>';
|
| }
|
| $item_data[] = array(
|
| 'name' => $label,
|
| 'value' => implode('<br>', $links),
|
| );
|
| } else {
|
| $item_data[] = array(
|
| 'name' => $label,
|
| 'value' => esc_html($value),
|
| );
|
| }
|
| }
|
|
|
| return $item_data;
|
| }, 20, 2 );
|
|
|
| /* =========================================================
|
| * Save to order item meta (unchanged)
|
| * ========================================================= */
|
| add_action( 'woocommerce_checkout_create_order_line_item', function( $item, $cart_item_key, $values, $order ) {
|
| if ( empty($values['merchatura_custom_fields']) || ! is_array($values['merchatura_custom_fields']) ) return;
|
|
|
| foreach ( $values['merchatura_custom_fields'] as $f ) {
|
| $label = $f['label'] ?? '';
|
| $type = $f['type'] ?? '';
|
| $value = $f['value'] ?? '';
|
|
|
| if ( $type === 'file' && is_array($value) ) {
|
| $urls = array();
|
| foreach ($value as $file) {
|
| if ( ! empty($file['url']) ) $urls[] = esc_url_raw($file['url']);
|
| }
|
| if ( $label && ! empty($urls) ) {
|
| $item->add_meta_data( $label, implode("\n", $urls), true );
|
| }
|
| } elseif ( $label && $value ) {
|
| $item->add_meta_data( $label, sanitize_text_field($value), true );
|
| }
|
| }
|
| }, 20, 4 );
|
|
|
| /* =========================================================
|
| * v4.1 ADD-ON: Export / Import / Copy field settings
|
| * ✅ Updated: conditions included automatically (already part of field array)
|
| * ========================================================= */
|
|
|
| add_action('admin_footer', function () {
|
| $screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
| if (!$screen || $screen->id !== 'product') return;
|
|
|
| global $post;
|
| if (!$post || $post->post_type !== 'product') return;
|
|
|
| $nonce = wp_create_nonce('merchatura_cf_tools');
|
| ?>
|
| <style>
|
| .mcf-tools-wrap { margin-top: 14px; padding-top: 14px; border-top: 1px solid #e5e5e5; }
|
| .mcf-tools-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
| .mcf-tools-card { border: 1px solid #e5e5e5; border-radius: 10px; background: #fff; padding: 12px; }
|
| .mcf-tools-card h4 { margin: 0 0 8px; font-size: 13px; }
|
| .mcf-tools-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
| .mcf-tools-card textarea { width: 100%; min-height: 120px; }
|
| .mcf-tools-note { margin-top: 8px; color:#666; font-size: 12px; }
|
| .mcf-tools-search { width: 100%; max-width: 420px; }
|
| .mcf-tools-results { margin-top: 8px; border: 1px solid #e5e5e5; border-radius: 10px; overflow: hidden; max-height: 220px; overflow-y: auto; display:none; }
|
| .mcf-tools-results button { width:100%; text-align:left; padding:10px 12px; border:0; background:#fff; cursor:pointer; }
|
| .mcf-tools-results button:hover { background:#f6f6f6; }
|
| @media (max-width: 1100px){ .mcf-tools-grid{ grid-template-columns: 1fr; } }
|
| </style>
|
|
|
| <script>
|
| (function(){
|
| const panel = document.getElementById('merchatura_custom_fields_panel');
|
| if(!panel) return;
|
|
|
| const productId = <?php echo (int) $post->ID; ?>;
|
| const ajaxUrl = "<?php echo esc_js(admin_url('admin-ajax.php')); ?>";
|
| const nonce = "<?php echo esc_js($nonce); ?>";
|
|
|
| const accordion = document.getElementById('merchatura-cf-accordion');
|
| if(!accordion) return;
|
|
|
| const wrap = document.createElement('div');
|
| wrap.className = 'mcf-tools-wrap';
|
| wrap.innerHTML = `
|
| <div class="mcf-tools-grid">
|
|
|
| <div class="mcf-tools-card">
|
| <h4>Export fields</h4>
|
| <div class="mcf-tools-row">
|
| <button type="button" class="button" id="mcf-export-btn">Export JSON</button>
|
| </div>
|
| <div class="mcf-tools-note">Downloads this product’s field setup as a .json file.</div>
|
| </div>
|
|
|
| <div class="mcf-tools-card">
|
| <h4>Import fields</h4>
|
| <textarea id="mcf-import-json" placeholder='Paste JSON here (or copy from a previous export)…'></textarea>
|
| <div class="mcf-tools-row" style="margin-top:10px;">
|
| <button type="button" class="button button-primary" id="mcf-import-btn">Import (overwrite)</button>
|
| </div>
|
| <div class="mcf-tools-note">This overwrites the current product’s fields.</div>
|
| </div>
|
|
|
| <div class="mcf-tools-card" style="grid-column: 1 / -1;">
|
| <h4>Copy fields from another product</h4>
|
| <input class="mcf-tools-search" type="text" id="mcf-copy-search" placeholder="Search product by name…">
|
| <div class="mcf-tools-results" id="mcf-copy-results"></div>
|
| <div class="mcf-tools-note">Search and click a product to copy its field setup onto this product (overwrite).</div>
|
| </div>
|
|
|
| </div>
|
| `;
|
| accordion.parentNode.appendChild(wrap);
|
|
|
| function post(action, data){
|
| return fetch(ajaxUrl, {
|
| method: 'POST',
|
| headers: {'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
|
| body: new URLSearchParams(Object.assign({action, nonce, product_id: productId}, data||{})).toString()
|
| }).then(r => r.json());
|
| }
|
|
|
| function download(filename, text){
|
| const blob = new Blob([text], {type: 'application/json'});
|
| const url = URL.createObjectURL(blob);
|
| const a = document.createElement('a');
|
| a.href = url;
|
| a.download = filename;
|
| document.body.appendChild(a);
|
| a.click();
|
| a.remove();
|
| URL.revokeObjectURL(url);
|
| }
|
|
|
| document.getElementById('mcf-export-btn').addEventListener('click', async function(){
|
| const res = await post('merchatura_cf_export_fields', {});
|
| if(!res || !res.success){
|
| alert(res?.data?.message || 'Export failed.');
|
| return;
|
| }
|
| download(`product-${productId}-custom-fields.json`, JSON.stringify(res.data.fields, null, 2));
|
| });
|
|
|
| document.getElementById('mcf-import-btn').addEventListener('click', async function(){
|
| const json = (document.getElementById('mcf-import-json').value || '').trim();
|
| if(!json){
|
| alert('Paste JSON first.');
|
| return;
|
| }
|
| const res = await post('merchatura_cf_import_fields', {json});
|
| if(!res || !res.success){
|
| alert(res?.data?.message || 'Import failed.');
|
| return;
|
| }
|
| alert('Imported! Reloading…');
|
| window.location.reload();
|
| });
|
|
|
| const search = document.getElementById('mcf-copy-search');
|
| const results = document.getElementById('mcf-copy-results');
|
| let t = null;
|
|
|
| search.addEventListener('input', function(){
|
| clearTimeout(t);
|
| const q = (search.value || '').trim();
|
| if(q.length < 2){
|
| results.style.display = 'none';
|
| results.innerHTML = '';
|
| return;
|
| }
|
| t = setTimeout(async () => {
|
| const res = await post('merchatura_cf_search_products', {q});
|
| if(!res || !res.success){
|
| results.style.display = 'none';
|
| results.innerHTML = '';
|
| return;
|
| }
|
| const items = res.data.items || [];
|
| if(!items.length){
|
| results.style.display = 'none';
|
| results.innerHTML = '';
|
| return;
|
| }
|
| results.innerHTML = items.map(i => `<button type="button" data-id="${i.id}">${i.text}</button>`).join('');
|
| results.style.display = 'block';
|
|
|
| results.querySelectorAll('button').forEach(btn => {
|
| btn.addEventListener('click', async function(){
|
| const fromId = btn.getAttribute('data-id');
|
| if(!fromId) return;
|
|
|
| if(!confirm('Copy fields from this product and overwrite current fields?')) return;
|
|
|
| const res2 = await post('merchatura_cf_copy_fields', {from_product_id: fromId});
|
| if(!res2 || !res2.success){
|
| alert(res2?.data?.message || 'Copy failed.');
|
| return;
|
| }
|
| alert('Copied! Reloading…');
|
| window.location.reload();
|
| });
|
| });
|
|
|
| }, 250);
|
| });
|
|
|
| })();
|
| </script>
|
| <?php
|
| });
|
|
|
| /* ---------------------------
|
| * AJAX: Export
|
| * --------------------------- */
|
| add_action('wp_ajax_merchatura_cf_export_fields', function(){
|
| if ( ! current_user_can('edit_products') ) {
|
| wp_send_json_error(array('message' => 'Permission denied.'), 403);
|
| }
|
| if ( ! isset($_POST['nonce']) || ! wp_verify_nonce(sanitize_text_field($_POST['nonce']), 'merchatura_cf_tools') ) {
|
| wp_send_json_error(array('message' => 'Bad nonce.'), 403);
|
| }
|
|
|
| $product_id = isset($_POST['product_id']) ? (int) $_POST['product_id'] : 0;
|
| if ( $product_id <= 0 ) wp_send_json_error(array('message' => 'Invalid product.'), 400);
|
|
|
| $fields = get_post_meta($product_id, MERCHATURA_FIELDS_META_KEY_V4, true);
|
| if ( ! is_array($fields) ) $fields = array();
|
|
|
| wp_send_json_success(array('fields' => $fields));
|
| });
|
|
|
| /* ---------------------------
|
| * AJAX: Import (overwrite)
|
| * --------------------------- */
|
| add_action('wp_ajax_merchatura_cf_import_fields', function(){
|
| if ( ! current_user_can('edit_products') ) {
|
| wp_send_json_error(array('message' => 'Permission denied.'), 403);
|
| }
|
| if ( ! isset($_POST['nonce']) || ! wp_verify_nonce(sanitize_text_field($_POST['nonce']), 'merchatura_cf_tools') ) {
|
| wp_send_json_error(array('message' => 'Bad nonce.'), 403);
|
| }
|
|
|
| $product_id = isset($_POST['product_id']) ? (int) $_POST['product_id'] : 0;
|
| $json = isset($_POST['json']) ? wp_unslash($_POST['json']) : '';
|
|
|
| if ( $product_id <= 0 ) wp_send_json_error(array('message' => 'Invalid product.'), 400);
|
| if ( trim($json) === '' ) wp_send_json_error(array('message' => 'No JSON provided.'), 400);
|
|
|
| $decoded = json_decode($json, true);
|
| if ( ! is_array($decoded) ) {
|
| wp_send_json_error(array('message' => 'Invalid JSON.'), 400);
|
| }
|
|
|
| $allowed = array('text','textarea','email','select','radio','file');
|
| $allowed_ops = array( 'is','is_not','contains','has_file','no_file' );
|
| $allowed_actions = array( 'show','hide' );
|
|
|
| $clean = array();
|
| $seen = array();
|
|
|
| foreach ($decoded as $f) {
|
| $type = isset($f['type']) ? sanitize_key($f['type']) : 'text';
|
| if ( ! in_array($type, $allowed, true) ) $type = 'text';
|
|
|
| $name = isset($f['name']) ? sanitize_text_field($f['name']) : '';
|
| if ($name === '') continue;
|
|
|
| $key = isset($f['key']) ? sanitize_key($f['key']) : '';
|
| if ($key === '') $key = sanitize_key(strtolower(preg_replace('/\s+/', '_', $name)));
|
| if ($key === '') continue;
|
|
|
| $base = $key; $i = 2;
|
| while (isset($seen[$key])) { $key = $base . '_' . $i; $i++; }
|
| $seen[$key] = true;
|
|
|
| $box3 = isset($f['box3']) ? trim((string)$f['box3']) : '';
|
| $info = isset($f['info']) ? trim((string)$f['info']) : '';
|
| $required = (isset($f['required']) && $f['required'] === 'yes') ? 'yes' : 'no';
|
|
|
| if ($type === 'select' || $type === 'radio') {
|
| $box3 = str_replace(array("\r\n","\r"), "\n", $box3);
|
| $lines = array_filter(array_map('trim', explode("\n", $box3)));
|
| $box3 = implode("\n", $lines);
|
| }
|
|
|
| if ($type === 'file') {
|
| $box3 = strtolower($box3);
|
| $box3 = preg_replace('/[^a-z0-9,\s]/', '', $box3);
|
| $box3 = preg_replace('/\s+/', '', $box3);
|
| }
|
|
|
| $conds_clean = array();
|
| if ( isset($f['conditions']) && is_array($f['conditions']) ) {
|
| foreach ( $f['conditions'] as $c ) {
|
| $action = isset($c['action']) ? sanitize_key($c['action']) : 'show';
|
| if ( ! in_array($action, $allowed_actions, true) ) $action = 'show';
|
|
|
| $source = isset($c['source']) ? sanitize_text_field($c['source']) : '';
|
| $source = trim($source);
|
|
|
| $op = isset($c['op']) ? sanitize_key($c['op']) : 'is';
|
| if ( ! in_array($op, $allowed_ops, true) ) $op = 'is';
|
|
|
| $value = isset($c['value']) ? sanitize_text_field($c['value']) : '';
|
| $value = trim($value);
|
|
|
| if ( $op === 'has_file' || $op === 'no_file' ) $value = '';
|
| if ( $source === '' ) continue;
|
|
|
| $conds_clean[] = array(
|
| 'action' => $action,
|
| 'source' => $source,
|
| 'op' => $op,
|
| 'value' => $value,
|
| );
|
| }
|
| }
|
|
|
| $clean[] = array(
|
| 'type' => $type,
|
| 'name' => $name,
|
| 'box3' => $box3,
|
| 'required' => $required,
|
| 'info' => $info,
|
| 'key' => $key,
|
| 'conditions' => $conds_clean,
|
| );
|
| }
|
|
|
| update_post_meta($product_id, MERCHATURA_FIELDS_META_KEY_V4, $clean);
|
| wp_send_json_success(array('message' => 'Imported.'));
|
| });
|
|
|
| /* ---------------------------
|
| * AJAX: Search products by title
|
| * --------------------------- */
|
| add_action('wp_ajax_merchatura_cf_search_products', function(){
|
| if ( ! current_user_can('edit_products') ) {
|
| wp_send_json_error(array('message' => 'Permission denied.'), 403);
|
| }
|
| if ( ! isset($_POST['nonce']) || ! wp_verify_nonce(sanitize_text_field($_POST['nonce']), 'merchatura_cf_tools') ) {
|
| wp_send_json_error(array('message' => 'Bad nonce.'), 403);
|
| }
|
|
|
| $q = isset($_POST['q']) ? sanitize_text_field($_POST['q']) : '';
|
| $q = trim($q);
|
| if ( strlen($q) < 2 ) wp_send_json_success(array('items' => array()));
|
|
|
| $args = array(
|
| 'post_type' => 'product',
|
| 'post_status' => array('publish','private','draft'),
|
| 's' => $q,
|
| 'posts_per_page' => 15,
|
| 'fields' => 'ids',
|
| );
|
|
|
| $ids = get_posts($args);
|
| $items = array();
|
|
|
| foreach ($ids as $id) {
|
| $title = get_the_title($id);
|
| if (!$title) continue;
|
| $items[] = array(
|
| 'id' => (int) $id,
|
| 'text' => $title . ' (ID: ' . (int)$id . ')'
|
| );
|
| }
|
|
|
| wp_send_json_success(array('items' => $items));
|
| });
|
|
|
| /* ---------------------------
|
| * AJAX: Copy fields from another product (overwrite)
|
| * --------------------------- */
|
| add_action('wp_ajax_merchatura_cf_copy_fields', function(){
|
| if ( ! current_user_can('edit_products') ) {
|
| wp_send_json_error(array('message' => 'Permission denied.'), 403);
|
| }
|
| if ( ! isset($_POST['nonce']) || ! wp_verify_nonce(sanitize_text_field($_POST['nonce']), 'merchatura_cf_tools') ) {
|
| wp_send_json_error(array('message' => 'Bad nonce.'), 403);
|
| }
|
|
|
| $product_id = isset($_POST['product_id']) ? (int) $_POST['product_id'] : 0;
|
| $from_id = isset($_POST['from_product_id']) ? (int) $_POST['from_product_id'] : 0;
|
|
|
| if ($product_id <= 0 || $from_id <= 0) {
|
| wp_send_json_error(array('message' => 'Invalid product IDs.'), 400);
|
| }
|
|
|
| $fields = get_post_meta($from_id, MERCHATURA_FIELDS_META_KEY_V4, true);
|
| if ( ! is_array($fields) ) $fields = array();
|
|
|
| update_post_meta($product_id, MERCHATURA_FIELDS_META_KEY_V4, $fields);
|
| wp_send_json_success(array('message' => 'Copied.'));
|
| });
|
|
|
| |
| |
Comments