Files
hoponhopofftickets/viator-collector/assets/js/vc-structure-admin.js
Amir Bagheri 930b7e0534 fix: paginated region/city pages, no routes before first save, safer structure save
- Add /{slug}/page/N/ rules and keep the page number through 301s
- Route only regions synced to a term, so unsaved presets never claim /europe/ etc.
- Flush rewrites on the request after activation so structure routes survive
- Suggest the existing term slug for new cities (keeps live /tour-destination/ URLs)
- Refuse saves without the editor sentinel; confirm before saving an empty structure
- Show term-sync failures to the admin instead of reporting success

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 15:33:01 +03:30

182 lines
5.5 KiB
JavaScript

/**
* Settings → Viator Structure editor. Regions and cities are plain DOM
* blocks; cities can only be added from the Viator destination search, and
* indexed field names are assigned on submit.
*/
(function () {
'use strict';
var cfg = window.vcStructure || {};
var i18n = cfg.i18n || {};
var form = document.getElementById('vc-structure-form');
var list = document.getElementById('vc-regions');
if (!form || !list) {
return;
}
function slugify(s) {
return String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
function fromTemplate(id) {
var tpl = document.getElementById(id);
return tpl.content.firstElementChild.cloneNode(true);
}
function usedIds() {
var ids = {};
list.querySelectorAll('.vc-city [data-f="id"]').forEach(function (input) {
ids[input.value] = true;
});
return ids;
}
function move(el, dir) {
if (dir < 0 && el.previousElementSibling) {
el.parentNode.insertBefore(el, el.previousElementSibling);
} else if (dir > 0 && el.nextElementSibling) {
el.parentNode.insertBefore(el.nextElementSibling, el);
}
}
function addCity(region, item) {
var row = fromTemplate('vc-tpl-city');
row.querySelector('[data-f="id"]').value = item.id;
row.querySelector('[data-f="name"]').value = item.name;
row.querySelector('[data-f="slug"]').value = item.slug || slugify(item.name);
row.querySelector('.vc-city__name').textContent = item.name;
row.querySelector('.vc-city__id').textContent = item.id;
region.querySelector('.vc-cities tbody').appendChild(row);
}
function search(region) {
var q = region.querySelector('.vc-city-search input[type="search"]');
var out = region.querySelector('.vc-results');
out.textContent = i18n.searching || '…';
var data = new FormData();
data.append('action', 'vc_search_destinations');
data.append('_ajax_nonce', cfg.nonce);
data.append('q', q.value);
fetch(cfg.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: data })
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res || !res.success) {
out.textContent = (res && res.data && res.data.message) || i18n.failed;
return;
}
if (!res.data.results.length) {
out.textContent = i18n.noMatches;
return;
}
var used = usedIds();
out.innerHTML = '';
res.data.results.forEach(function (item) {
var row = document.createElement('div');
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'button button-small';
if (used[item.id]) {
btn.disabled = true;
btn.textContent = i18n.used;
} else {
btn.textContent = i18n.add;
btn.addEventListener('click', function () {
addCity(region, item);
out.innerHTML = '';
q.value = '';
});
}
var label = document.createElement('span');
label.textContent = item.name + ' (ID ' + item.id + ')';
row.appendChild(btn);
row.appendChild(label);
out.appendChild(row);
});
})
.catch(function () { out.textContent = i18n.failed; });
}
list.addEventListener('click', function (e) {
var btn = e.target.closest('[data-act]');
if (!btn) {
return;
}
var region = btn.closest('.vc-region');
var city = btn.closest('.vc-city');
var target = city || region;
switch (btn.getAttribute('data-act')) {
case 'up': move(target, -1); break;
case 'down': move(target, 1); break;
case 'remove-city': city.remove(); break;
case 'remove-region':
if (window.confirm(i18n.confirm)) {
region.remove();
}
break;
case 'search': search(region); break;
}
});
list.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && e.target.matches('.vc-city-search input[type="search"]')) {
e.preventDefault();
search(e.target.closest('.vc-region'));
}
});
// Suggest a region slug from its name until the slug is edited by hand.
list.addEventListener('input', function (e) {
var input = e.target;
if (input.matches('.vc-region__head [data-f="slug"]')) {
input.dataset.touched = '1';
}
if (input.matches('.vc-region__head [data-f="name"]')) {
var slug = input.closest('.vc-region__head').querySelector('[data-f="slug"]');
if (!slug.dataset.touched && (slug.value === '' || slug.dataset.auto === '1')) {
slug.value = slugify(input.value);
slug.dataset.auto = '1';
}
}
});
document.getElementById('vc-add-region').addEventListener('click', function () {
var region = fromTemplate('vc-tpl-region');
list.appendChild(region);
region.querySelector('[data-f="name"]').focus();
});
function hidden(name) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = '1';
form.appendChild(input);
}
form.addEventListener('submit', function (e) {
var regions = list.querySelectorAll('.vc-region');
if (!regions.length) {
if (!window.confirm(i18n.confirmEmpty)) {
e.preventDefault();
return;
}
hidden('vc_confirm_empty');
}
// Proves the editor ran: the server refuses a save without it, so a
// failed script can never submit an empty structure by accident.
hidden('vc_structure_present');
regions.forEach(function (region, i) {
region.querySelectorAll('.vc-region__head [data-f]').forEach(function (input) {
input.name = 'vc_structure[' + i + '][' + input.dataset.f + ']';
});
region.querySelectorAll('.vc-city').forEach(function (city, j) {
city.querySelectorAll('[data-f]').forEach(function (input) {
input.name = 'vc_structure[' + i + '][cities][' + j + '][' + input.dataset.f + ']';
});
});
});
});
})();