admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce(self::NONCE),
]);
wp_enqueue_style('ms-ajax-menu-saver-css', plugin_dir_url(__FILE__).'ms-ajax.css', [], '1.0.0');
}
public function render_page() {
if ( ! current_user_can('edit_theme_options')) {
wp_die(__('Non hai i permessi per gestire i menu.', 'ms-ajax-menu-saver'));
}
$menus = wp_get_nav_menus();
?>
Menu Saver (AJAX)
Modifica e salva i menu in modo sicuro, evitando timeout. Le modifiche vengono salvate in batch.
ID
Etichetta
URL
Ordine
Parent ID
Target
Titolo (attr)
Azione
'Permesso negato.']);
}
}
public function ajax_load_menu_items() {
$this->verify();
$menu_id = isset($_POST['menu_id']) ? intval($_POST['menu_id']) : 0;
if (!$menu_id) wp_send_json_error(['message' => 'menu_id mancante']);
$items = wp_get_nav_menu_items($menu_id, ['update_post_term_cache' => false]);
$out = [];
if ($items) {
foreach ($items as $it) {
$out[] = [
'id' => intval($it->ID),
'title' => $it->title,
'url' => $it->url,
'menu_order' => intval($it->menu_order),
'menu_item_parent' => intval($it->menu_item_parent),
'attr_title' => get_post_meta($it->ID, '_menu_item_attr_title', true),
'target' => get_post_meta($it->ID, '_menu_item_target', true),
'status' => get_post_status($it->ID),
];
}
}
wp_send_json_success([
'items' => $out,
'message' => 'Menu caricato',
]);
}
public function ajax_save_menu_batch() {
$this->verify();
@set_time_limit(120);
$menu_id = isset($_POST['menu_id']) ? intval($_POST['menu_id']) : 0;
$batch = isset($_POST['batch']) ? (array) $_POST['batch'] : [];
$to_del = isset($_POST['delete_ids']) ? (array) $_POST['delete_ids'] : [];
if (!$menu_id) wp_send_json_error(['message' => 'menu_id mancante']);
$results = ['updated'=>[], 'created'=>[], 'deleted'=>[], 'errors'=>[]];
// DELETE
foreach ($to_del as $del_id) {
$del_id = intval($del_id);
if ($del_id > 0) {
$ok = wp_delete_post($del_id, true);
if ($ok) $results['deleted'][] = $del_id;
else $results['errors'][] = "Impossibile eliminare ID $del_id";
}
}
// UPDATE / CREATE
foreach ($batch as $row) {
// row fields
$raw_id = isset($row['id']) ? $row['id'] : '';
$title = isset($row['title']) ? wp_strip_all_tags($row['title']) : '';
$url = isset($row['url']) ? esc_url_raw($row['url']) : '';
$order = isset($row['menu_order']) ? intval($row['menu_order']) : 0;
$parent = isset($row['menu_item_parent']) ? intval($row['menu_item_parent']) : 0;
$attr_t = isset($row['attr_title']) ? sanitize_text_field($row['attr_title']) : '';
$target = isset($row['target']) ? sanitize_text_field($row['target']) : '';
$is_new = (strpos($raw_id, 'new-') === 0);
// Forziamo parent >=0 (nuove voci solo top-level per semplicità)
if ($is_new) $parent = 0;
$args = [
'menu-item-title' => $title,
'menu-item-url' => $url,
'menu-item-status' => 'publish',
'menu-item-position' => $order,
'menu-item-parent-id' => $parent,
'menu-item-attr-title' => $attr_t,
'menu-item-target' => $target === '_blank' ? '_blank' : '',
'menu-item-type' => 'custom',
];
if ($is_new) {
$item_id = wp_update_nav_menu_item($menu_id, 0, $args);
if (is_wp_error($item_id)) {
$results['errors'][] = "Errore creazione '{$title}': ".$item_id->get_error_message();
} else {
$results['created'][] = ['temp_id'=>$raw_id, 'real_id'=>$item_id];
}
} else {
$item_id = intval($raw_id);
$res = wp_update_nav_menu_item($menu_id, $item_id, $args);
if (is_wp_error($res)) {
$results['errors'][] = "Errore aggiornamento ID {$item_id}: ".$res->get_error_message();
} else {
$results['updated'][] = $item_id;
}
}
}
wp_send_json_success($results);
}
public function ajax_force_publish() {
$this->verify();
@set_time_limit(120);
global $wpdb;
$menu_id = isset($_POST['menu_id']) ? intval($_POST['menu_id']) : 0;
if (!$menu_id) wp_send_json_error(['message'=>'menu_id mancante']);
// Trova tutti i post nav_menu_item collegati a quel menu ma non 'publish'
$sql = "
SELECT p.ID
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.term_id = %d
AND tt.taxonomy = 'nav_menu'
AND p.post_type = 'nav_menu_item'
AND p.post_status <> 'publish'
";
$ids = $wpdb->get_col($wpdb->prepare($sql, $menu_id));
$changed = [];
if ($ids) {
foreach ($ids as $pid) {
wp_update_post([
'ID' => intval($pid),
'post_status' => 'publish'
]);
$changed[] = intval($pid);
}
}
wp_send_json_success([
'changed' => $changed,
'message' => $changed ? 'Voci pubblicate forzatamente.' : 'Nessuna voce non-pubblicata trovata.'
]);
}
}
new MS_Ajax_Menu_Saver();
/**
* Assets inline (JS/CSS) serviti dal plugin directory.
* Se salvi come unico file, WordPress li caricherà da qui; in alternativa crea i file ms-ajax.js e ms-ajax.css.
*/
add_action('admin_init', function() {
$base = plugin_dir_path(__FILE__);
// Crea i file se non esistono (facilita copia/incolla singolo file)
if ( is_writable($base) ) {
$js = $base.'ms-ajax.js';
if ( !file_exists($js) ) {
file_put_contents($js, <<{ st.fadeOut(); }, 4000);
}
function uid() {
return 'new-' + Math.random().toString(36).slice(2,11);
}
function render(){
tbody.empty();
rows.sort((a,b)=> (a.menu_order||0) - (b.menu_order||0));
rows.forEach(r=>{
const tr = $('
');
tr.append('
'+r.id+'
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.append('
');
tr.data('row-id', r.id);
tbody.append(tr);
});
editor.removeClass('hidden');
}
function collectBatch(start, size){
const slice = rows.slice(start, start+size);
return slice.map(r=>({
id: r.id,
title: r.title||'',
url: r.url||'',
menu_order: parseInt(r.menu_order||0,10),
menu_item_parent: parseInt(r.menu_item_parent||0,10),
target: r.target||'',
attr_title: r.attr_title||'',
}));
}
function applyTableToRows(){
tbody.find('tr').each(function(){
const id = $(this).data('row-id');
const r = rows.find(x=>x.id===id);
if (!r) return;
r.title = $(this).find('.ms-title').val();
r.url = $(this).find('.ms-url').val();
r.menu_order = parseInt($(this).find('.ms-order').val()||0,10);
r.target = $(this).find('.ms-target').val();
r.attr_title = $(this).find('.ms-attr').val();
if (!String(id).startsWith('new-')) {
r.menu_item_parent = parseInt($(this).find('.ms-parent').val()||0,10);
}
});
}
$('#msams-load').on('click', function(){
const menuId = $('#msams-menu-select').val();
if (!menuId) { msg('Seleziona un menu.', 'error'); return; }
currentMenuId = menuId;
st.text('Caricamento...').show();
$.post(MSAMS.ajaxUrl, {
action: 'ms_load_menu_items',
nonce: MSAMS.nonce,
menu_id: menuId
}, function(resp){
if (!resp || !resp.success) { msg(resp && resp.data && resp.data.message ? resp.data.message : 'Errore di caricamento', 'error'); return; }
rows = resp.data.items || [];
deleted = [];
render();
msg('Menu caricato');
});
});
$('#msams-add-item').on('click', function(){
if (!currentMenuId) { msg('Prima carica un menu.', 'error'); return; }
rows.push({
id: uid(),
title: 'Nuova voce',
url: 'https://',
menu_order: (rows.length? (Math.max.apply(null, rows.map(r=>r.menu_order||0))+1):0),
menu_item_parent: 0,
attr_title: '',
target: ''
});
render();
});
tbody.on('click', '.link-delete', function(){
const id = $(this).closest('tr').data('row-id');
const idx = rows.findIndex(x=>x.id===id);
if (idx>-1) {
const r = rows[idx];
if (!String(r.id).startsWith('new-')) { deleted.push(r.id); }
rows.splice(idx,1);
render();
}
});
$('#msams-save').on('click', async function(){
if (!currentMenuId) { msg('Prima carica un menu.', 'error'); return; }
applyTableToRows();
const batchSize = 25;
let start = 0;
let createdMap = {};
while (start < rows.length) {
const payload = collectBatch(start, batchSize);
const resp = await $.post(MSAMS.ajaxUrl, {
action: 'ms_save_menu_batch',
nonce: MSAMS.nonce,
menu_id: currentMenuId,
batch: payload,
delete_ids: start===0 ? deleted : [] // inviamo delete solo nel primo giro
});
if (!resp || !resp.success) { msg('Errore salvataggio batch', 'error'); return; }
if (resp.data && resp.data.created) {
resp.data.created.forEach(pair=>{
// sostituisce temp id con real id
rows.forEach(r=>{
if (r.id === pair.temp_id) r.id = pair.real_id;
});
createdMap[pair.temp_id] = pair.real_id;
});
}
start += batchSize;
st.text('Salvataggio... '+Math.min(start, rows.length)+' / '+rows.length+' voci').show();
}
deleted = [];
msg('Salvataggio completato');
// Ricarica per sicurezza e avere dati aggiornati
$('#msams-load').click();
});
$('#msams-export').on('click', function(){
applyTableToRows();
jsonBox.removeClass('hidden').val(JSON.stringify(rows, null, 2));
msg('Esportato in JSON qui sotto');
});
$('#msams-import').on('click', function(){
const val = prompt('Incolla JSON qui (verrà sovrascritto l’elenco attuale). Confermi? Scrivi SI per continuare.', '');
if (val !== 'SI') return;
jsonBox.removeClass('hidden');
const pasted = prompt('Incolla ora il JSON e premi OK', '');
if (!pasted) return;
try {
const data = JSON.parse(pasted);
if (Array.isArray(data)) {
rows = data;
deleted = [];
render();
msg('JSON importato.');
} else {
msg('Formato JSON non valido.', 'error');
}
} catch(e){
msg('JSON non valido: '+e.message, 'error');
}
});
$('#msams-force-publish').on('click', function(){
const menuId = $('#msams-menu-select').val();
if (!menuId) { msg('Seleziona un menu.', 'error'); return; }
st.text('Forzo pubblicazione voci...').show();
$.post(MSAMS.ajaxUrl, {
action: 'ms_force_publish_menu_items',
nonce: MSAMS.nonce,
menu_id: menuId
}, function(resp){
if (!resp || !resp.success) { msg('Errore nel forzare la pubblicazione', 'error'); return; }
msg(resp.data.message || 'Operazione completata');
$('#msams-load').click();
});
});
})(jQuery);
JS
);
}
$css = $base.'ms-ajax.css';
if ( !file_exists($css) ) {
file_put_contents($css, <<
Marcianise - Clima di alta tensione nello stabilimento Jabil di Marcianise, dove un "ultimatum" aziendale ha innescato una nuova, veemente reazione da parte dei lavoratori. Questa mattina, la forza lavoro ha nuovamente incrociato le braccia, bloccando la produzione per un'ora in segno di aperta contestazione.
La miccia è stata la...