Files
ave-cms/adminx/modules/Catalog/assets/catalog.js
T
2026-07-27 12:58:44 +03:00

196 lines
39 KiB
JavaScript

(function (window, document) {
'use strict';
var Adminx = window.Adminx || (window.Adminx = {});
Adminx.Catalog = {
itemForm: null, settingsForm: null, dragRow: null, dragParent: null, dragSnapshot: '', dragTarget: null, filterOrder: [], itemSaveTimer: null, settingsSaveTimer: null, conditionContext: null,
init: function () {
this.itemForm = document.getElementById('catalogItemForm');
this.settingsForm = document.getElementById('catalogSettingsForm');
var self = this;
document.addEventListener('click', function (e) {
var pageTab = e.target.closest('[data-catalog-page-tab]');
if (pageTab) { self.tab('[data-catalog-page-tab]', '[data-catalog-page-panel]', pageTab.getAttribute('data-catalog-page-tab')); }
var drawerTab = e.target.closest('[data-catalog-drawer-tab]');
if (drawerTab) { self.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', drawerTab.getAttribute('data-catalog-drawer-tab')); }
if (e.target.closest('[data-catalog-item-new]')) { self.newItem(); }
var edit = e.target.closest('[data-catalog-item-edit]');
if (edit) { self.editItem(edit.getAttribute('data-catalog-item-edit')); }
var del = e.target.closest('[data-catalog-item-delete]');
if (del) { self.deleteItem(del.getAttribute('data-catalog-item-delete')); }
if (e.target.closest('[data-catalog-document-pick]')) { self.openDocumentPicker(); }
if (e.target.closest('[data-catalog-document-clear]')) { self.setDocument(null); self.scheduleItemSave(); }
var conditionView = e.target.closest('[data-catalog-condition-view]');
if (conditionView) { self.showCondition(conditionView.getAttribute('data-catalog-condition-view')); }
var conditionSync = e.target.closest('[data-catalog-condition-sync]');
if (conditionSync) { self.syncCondition(conditionSync.getAttribute('data-catalog-condition-sync')); }
if (e.target.closest('[data-catalog-conditions-sync]')) { self.syncMissingConditions(); }
if (e.target.closest('[data-catalog-filter-order]')) { self.openFilterOrder(); }
if (e.target.closest('[data-catalog-filter-recompile]')) { self.recompileCurrent(); }
if (e.target.closest('[data-catalog-recompile-all]')) { self.recompileAll(); }
});
document.addEventListener('input', function (e) {
if (e.target.matches('[data-catalog-tree-search]')) { self.search(e.target.value); }
});
document.addEventListener('change', function (e) {
if (e.target.matches('[data-catalog-item-status]')) { self.setTreeStatus(e.target); return; }
if (self.itemForm && e.target.closest('#catalogItemForm')) {
if (e.target.matches('[name="filters_use[]"]')) { self.syncFilterOrderSelection(); }
self.updateCounts();
if (e.target.matches('[name="status"], [name="parent_id"], [name="fields_use[]"], [name="filters_use[]"], [name^="filter_style["]')) { self.scheduleItemSave(); }
return;
}
if (self.settingsForm && e.target.closest('#catalogSettingsForm')) { self.updateSettingsGroups(); self.updateCommerceSettings(); self.scheduleSettingsSave(); }
});
if (this.itemForm) { this.itemForm.addEventListener('submit', function (e) { e.preventDefault(); self.saveItem(false); }); }
if (this.settingsForm) { this.settingsForm.addEventListener('submit', function (e) { e.preventDefault(); self.saveSettings(false); }); this.updateSettingsGroups(); this.updateCommerceSettings(); }
var createForm = document.querySelector('[data-catalog-create]');
if (createForm) { createForm.addEventListener('submit', function (e) { e.preventDefault(); self.createCatalog(createForm); }); }
document.addEventListener('dragstart', function (e) { self.dragStart(e); });
document.addEventListener('dragover', function (e) { self.dragOver(e); });
document.addEventListener('drop', function (e) { if (self.dragRow) { e.preventDefault(); } });
document.addEventListener('dragend', function () { self.dragEnd(); });
this.openRequestedItem();
},
base: function () { return (this.itemForm || this.settingsForm).getAttribute('data-base'); },
rubric: function () { return (this.itemForm || this.settingsForm).getAttribute('data-rubric'); },
field: function () { return (this.itemForm || this.settingsForm).getAttribute('data-field'); },
url: function () { return this.base() + '/catalog/' + this.rubric() + '/' + this.field(); },
csrf: function () { var el = (this.itemForm || this.settingsForm).querySelector('[name="_csrf"]'); return el ? el.value : ''; },
createCatalog: function (form) {
Adminx.Loader.show();
fetch(form.getAttribute('data-base') + '/catalog', { method: 'POST', body: new FormData(form), credentials: 'same-origin', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } })
.then(this.json).then(function (payload) { Adminx.Toast.show(payload.message || 'Каталог создан', 'success'); window.location.href = payload.redirect || (payload.data && payload.data.redirect); })
.catch(this.error).finally(function () { Adminx.Loader.hide(); });
},
tab: function (tabs, panels, value) {
document.querySelectorAll(tabs).forEach(function (el) { var active = el.getAttribute(tabs.indexOf('drawer') >= 0 ? 'data-catalog-drawer-tab' : 'data-catalog-page-tab') === value; el.classList.toggle('is-active', active); el.setAttribute('aria-selected', active ? 'true' : 'false'); });
document.querySelectorAll(panels).forEach(function (el) { el.hidden = el.getAttribute(panels.indexOf('drawer') >= 0 ? 'data-catalog-drawer-panel' : 'data-catalog-page-panel') !== value; });
},
newItem: function () {
this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = ''; this.itemForm.querySelector('[name="status"]').checked = true;
var defaultFields = this.settingsValues('fields_default[]'), defaultFilters = this.settingsValues('filters_default[]');
this.checkValues('fields_use[]', defaultFields); this.checkValues('filters_use[]', defaultFilters);
defaultFilters.forEach(function (id) { var source = this.settingsForm.querySelector('[name="filter_style[' + id + ']"]'), target = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (source && target) { target.value = source.value; } }, this);
this.filterOrder = defaultFilters.slice(); this.syncFilterOrderInput();
this.setDocument(null); this.setState('item', ''); this.renderConditionContext(null);
document.querySelector('[data-catalog-drawer-title]').textContent = 'Новый раздел'; this.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', 'main'); this.updateCounts();
Adminx.Drawer.open('catalogItemDrawer');
},
settingsValues: function (name) { return this.settingsForm ? Array.prototype.map.call(this.settingsForm.querySelectorAll('[name="' + name + '"]:checked'), function (input) { return String(input.value); }) : []; },
updateSettingsGroups: function () { if (!this.settingsForm) { return; } this.settingsForm.querySelectorAll('[data-catalog-settings-group]').forEach(function (group) { var name = group.getAttribute('data-catalog-settings-group'), selected = group.querySelectorAll('[name="' + name + '"]:checked').length, badge = group.querySelector('[data-catalog-settings-selected]'); group.querySelectorAll('.catalog-choice-row, .catalog-filter-row').forEach(function (row) { var input = row.querySelector('[name="' + name + '"]'); row.classList.toggle('is-selected', !!input && input.checked); }); if (badge) { badge.textContent = 'Включено: ' + selected; badge.classList.toggle('badge-blue', selected > 0); badge.classList.toggle('badge-gray', selected === 0); } }); },
updateCommerceSettings: function () { if (!this.settingsForm) { return; } var purpose = this.settingsForm.querySelector('[name="purpose"]'), visible = purpose && purpose.value === 'commerce'; this.settingsForm.querySelectorAll('[data-catalog-commerce-fields]').forEach(function (section) { section.hidden = !visible; }); },
editItem: function (id, requestedTab) {
var self = this; Adminx.Loader.show();
fetch(this.base() + '/catalog/items/' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json).then(function (payload) {
self.fill(payload.data || {}); Adminx.Drawer.open('catalogItemDrawer');
if (['main', 'fields', 'filters'].indexOf(requestedTab) >= 0) { self.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', requestedTab); }
}).catch(this.error).finally(function () { Adminx.Loader.hide(); });
},
openRequestedItem: function () {
if (!this.itemForm || typeof URLSearchParams === 'undefined') { return; }
var params = new URLSearchParams(window.location.search), itemId = params.get('item');
if (!itemId || !/^\d+$/.test(itemId)) { return; }
this.editItem(itemId, params.get('tab') || 'main');
},
fill: function (item) {
this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = item.id || ''; this.itemForm.querySelector('[name="name"]').value = item.name || '';
this.itemForm.querySelector('[name="parent_id"]').value = item.parent_id || 0; this.itemForm.querySelector('[name="status"]').checked = Number(item.status) === 1;
this.setDocument(item.document_id ? { id: item.document_id, title: item.document_title || '', alias: item.document_alias || '' } : null);
this.filterOrder = (item.filters_use || []).map(function (id) { return String(id); }); this.syncFilterOrderInput(); this.checkValues('fields_use[]', item.fields_use || []); this.checkValues('filters_use[]', item.filters_use || []);
Object.keys(item.filter_styles || {}).forEach(function (id) { var select = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (select) { select.value = item.filter_styles[id]; } }, this);
var own = this.itemForm.querySelector('[name="parent_id"] option[value="' + item.id + '"]'); if (own) { own.disabled = true; }
this.renderConditionContext(item.condition_context || null);
document.querySelector('[data-catalog-drawer-title]').textContent = item.name || 'Раздел каталога'; this.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', 'main'); this.updateCounts(); this.setState('item', '');
},
checkValues: function (name, values) { var map = {}; values.forEach(function (id) { map[String(id)] = true; }); this.itemForm.querySelectorAll('[name="' + name + '"]').forEach(function (el) { el.checked = !!map[el.value]; }); },
updateCounts: function () { ['fields', 'filters'].forEach(function (type) { var count = this.itemForm.querySelectorAll('[name="' + type + '_use[]"]:checked').length; var el = document.querySelector('[data-catalog-' + type + '-count]'); if (el) { el.textContent = count; } this.itemForm.querySelectorAll('[name="' + type + '_use[]"]').forEach(function (toggle) { var row = toggle.closest('.catalog-choice-row, .catalog-filter-row'); if (row) { row.classList.toggle('is-selected', toggle.checked); } }); this.itemForm.querySelectorAll('[data-catalog-field-group="' + type + '"]').forEach(function (group) { var selected = group.querySelectorAll('[name="' + type + '_use[]"]:checked').length; var badge = group.querySelector('[data-catalog-group-selected]'); if (badge) { badge.textContent = 'Включено: ' + selected; badge.classList.toggle('badge-blue', selected > 0); badge.classList.toggle('badge-gray', selected === 0); } }); }, this); var orderButton = this.itemForm.querySelector('[data-catalog-filter-order]'); if (orderButton) { orderButton.disabled = this.filterOrder.length < 2; } this.renderConditionContext(this.conditionContext); },
syncFilterOrderSelection: function () { var selected = {}, next = []; this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').forEach(function (input) { selected[input.value] = true; }); this.filterOrder.forEach(function (id) { if (selected[id]) { next.push(id); delete selected[id]; } }); this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').forEach(function (input) { if (selected[input.value]) { next.push(input.value); delete selected[input.value]; } }); this.filterOrder = next; this.syncFilterOrderInput(); },
syncFilterOrderInput: function () { var input = this.itemForm && this.itemForm.querySelector('[name="filters_order"]'); if (input) { input.value = this.filterOrder.join(','); } },
openFilterOrder: function () { var self = this; this.syncFilterOrderSelection(); if (this.filterOrder.length < 2) { return; } var overlay = document.createElement('div'), rows = ''; this.filterOrder.forEach(function (id) { var input = self.itemForm.querySelector('[name="filters_use[]"][value="' + id + '"]'), row = input.closest('.catalog-filter-row'), title = row.querySelector('.catalog-choice-copy b').textContent.trim(), type = row.querySelector('.catalog-field-type b').textContent.trim(); rows += '<div class="catalog-filter-order-row" data-filter-order-id="' + self.esc(id) + '"><button class="catalog-filter-order-handle" type="button" draggable="true" aria-label="Перетащить"><i class="ti ti-grip-vertical"></i></button><span><b>' + self.esc(title) + '</b><small>' + self.esc(type) + ' · #' + self.esc(id) + '</small></span></div>'; }); overlay.className = 'overlay catalog-filter-order-overlay'; overlay.innerHTML = '<div class="modal catalog-filter-order-modal" role="dialog" aria-modal="true"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-arrows-sort"></i></span><div><h3>Порядок фильтров</h3><p class="text-secondary">' + this.filterOrder.length + ' включённых фильтров.</p></div><button class="modal-close" type="button" data-filter-order-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div><div class="modal-body"><div class="catalog-filter-order-list">' + rows + '</div></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-filter-order-close>Закрыть</button><button class="btn btn-primary" type="button" data-filter-order-save><i class="ti ti-device-floppy"></i>Сохранить порядок</button></div></div>'; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); var dragged = null, close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); }; overlay.addEventListener('dragstart', function (e) { var handle = e.target.closest('.catalog-filter-order-handle'), row = handle ? handle.closest('[data-filter-order-id]') : null; if (!row) { e.preventDefault(); return; } dragged = row; row.classList.add('is-dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', row.getAttribute('data-filter-order-id')); }); overlay.addEventListener('dragover', function (e) { var target = e.target.closest('[data-filter-order-id]'); if (!dragged || !target || target === dragged) { return; } e.preventDefault(); var rect = target.getBoundingClientRect(), reference = e.clientY < rect.top + rect.height / 2 ? target : target.nextSibling; if (reference !== dragged && dragged.nextSibling !== reference) { target.parentNode.insertBefore(dragged, reference); } }); overlay.addEventListener('dragend', function () { if (dragged) { dragged.classList.remove('is-dragging'); dragged = null; } }); overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-filter-order-close]')) { close(); return; } if (e.target.closest('[data-filter-order-save]')) { self.filterOrder = Array.prototype.map.call(overlay.querySelectorAll('[data-filter-order-id]'), function (row) { return row.getAttribute('data-filter-order-id'); }); self.syncFilterOrderInput(); self.scheduleItemSave(); close(); } }); },
renderConditionContext: function (context) {
this.conditionContext = context || { request: { id: 0, title: '' }, states: {}, summary: { synced: 0, staged: 0, missing: 0, different: 0, unsupported: 0 } };
var request = this.conditionContext.request || {}, summary = this.conditionContext.summary || {}, requestEl = document.querySelector('[data-catalog-condition-request]'), summaryEl = document.querySelector('[data-catalog-condition-summary]'), bulk = document.querySelector('[data-catalog-conditions-sync]');
if (requestEl) { requestEl.textContent = Number(request.id) > 0 ? ('#' + request.id + ' · ' + (request.title || 'Без названия')) : 'Запрос фильтра не выбран'; }
if (summaryEl) { summaryEl.textContent = Number(request.id) > 0 ? ('Готово: ' + (summary.synced || 0) + ' · ждут проверки: ' + (summary.staged || 0) + ' · нет: ' + (summary.missing || 0) + ' · отличаются: ' + (summary.different || 0)) : 'Выберите запрос в настройках каталога'; }
if (bulk) { bulk.disabled = Number(request.id) <= 0 || Number(summary.missing) <= 0 || !this.itemForm.querySelector('[name="id"]').value; } var recompile = this.itemForm.querySelector('[data-catalog-filter-recompile]'); if (recompile) { recompile.disabled = !this.itemForm.querySelector('[name="id"]').value || this.filterOrder.length === 0; }
var states = this.conditionContext.states || {}, labels = { synced: 'Готово', staged: 'Ждёт Shadow', missing: 'Не создано', different: 'Отличается', unsupported: 'Недоступно', no_request: 'Нет запроса' }, classes = { synced: 'badge-green', staged: 'badge-blue', missing: 'badge-amber', different: 'badge-red', unsupported: 'badge-gray', no_request: 'badge-gray' };
this.itemForm.querySelectorAll('[data-catalog-condition-field]').forEach(function (cell) {
var id = cell.getAttribute('data-catalog-condition-field'), checked = this.itemForm.querySelector('[name="filters_use[]"][value="' + id + '"]').checked, state = checked ? states[id] : null, badge = cell.querySelector('[data-catalog-condition-status]'), view = cell.querySelector('[data-catalog-condition-view]'), sync = cell.querySelector('[data-catalog-condition-sync]');
badge.className = 'badge ' + (state ? (classes[state.state] || 'badge-gray') : 'badge-gray'); badge.textContent = state ? (labels[state.state] || state.state) : (checked ? 'Ожидает сохранения' : 'Не включён');
cell.setAttribute('data-condition-state', state ? state.state : 'disabled');
view.hidden = !(state && ((state.generated && state.generated.value) || state.existing));
sync.hidden = !(state && (state.state === 'missing' || state.state === 'different'));
}, this);
},
showCondition: function (fieldId) {
var state = this.conditionContext && this.conditionContext.states ? this.conditionContext.states[String(fieldId)] : null; if (!state) { return; }
var row = this.itemForm.querySelector('[data-catalog-condition-field="' + fieldId + '"]').closest('.catalog-filter-row'), title = row.querySelector('.catalog-choice-copy b').textContent.trim(), overlay = document.createElement('div'), generated = state.generated || {}, existing = state.existing || null;
overlay.className = 'overlay catalog-condition-overlay';
overlay.innerHTML = '<div class="modal catalog-condition-modal" role="dialog" aria-modal="true"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-code-dots"></i></span><div><h3>' + this.esc(title) + '</h3><p class="text-secondary">Условие поля #' + this.esc(fieldId) + ' в запросе #' + this.esc((this.conditionContext.request || {}).id || '') + '.</p></div><button class="modal-close" type="button" data-catalog-condition-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div><div class="modal-body"><section class="catalog-condition-section"><div class="catalog-condition-section-head"><h4>Сгенерированное условие</h4><span class="badge badge-blue">' + this.esc(generated.compare || 'нет') + '</span></div><pre class="catalog-condition-code">' + this.esc(generated.value || 'Автоматическая генерация для этого типа не поддерживается.') + '</pre></section><section class="catalog-condition-section"><div class="catalog-condition-section-head"><h4>Текущее условие запроса</h4><span class="badge ' + (existing ? 'badge-gray' : 'badge-amber') + '">' + this.esc(existing ? existing.compare : 'не создано') + '</span></div><pre class="catalog-condition-code">' + this.esc(existing ? existing.value : 'Условие отсутствует.') + '</pre></section></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-catalog-condition-close>Закрыть</button></div></div>';
document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); });
var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); };
overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-catalog-condition-close]')) { close(); } });
},
syncCondition: function (fieldId) {
var state = this.conditionContext && this.conditionContext.states ? this.conditionContext.states[String(fieldId)] : null, self = this; if (!state) { return; }
var run = function () { var data = new FormData(); data.append('_csrf', self.csrf()); if (state.state === 'different') { data.append('force', '1'); } self.ajax(self.url() + '/items/' + encodeURIComponent(self.itemForm.querySelector('[name="id"]').value) + '/conditions/' + encodeURIComponent(fieldId), data, function (payload) { self.renderConditionContext(payload.data.condition_context); Adminx.Toast.show(payload.message, 'success'); }); };
if (state.state === 'different' && Adminx.Confirm) { Adminx.Confirm.open({ kind: 'warning', title: 'Заменить условие?', message: 'Текущее выражение отличается от автоматически сгенерированного. Оно будет заменено, затем запрос будет пересобран.', confirmLabel: 'Заменить', confirmClass: 'btn-warning', onConfirm: run }); return; }
run();
},
syncMissingConditions: function () {
var id = this.itemForm.querySelector('[name="id"]').value, self = this; if (!id) { return; } var data = new FormData(); data.append('_csrf', this.csrf()); this.ajax(this.url() + '/items/' + encodeURIComponent(id) + '/conditions/sync', data, function (payload) { self.renderConditionContext(payload.data.condition_context); Adminx.Toast.show(payload.message, 'success'); });
},
recompileCurrent: function () { var id = this.itemForm.querySelector('[name="id"]').value, self = this; if (!id) { return; } var data = new FormData(); data.append('_csrf', this.csrf()); this.ajax(this.url() + '/items/' + encodeURIComponent(id) + '/filters/recompile', data, function (payload) { Adminx.Toast.show(payload.message, 'success'); }); },
recompileAll: function () { var self = this, ids = Array.prototype.filter.call(document.querySelectorAll('[data-catalog-item]'), function (item) { return Number(item.getAttribute('data-filter-count')) > 0; }).map(function (item) { return item.getAttribute('data-id'); }); if (!ids.length) { Adminx.Toast.show('В каталоге нет настроенных фильтров', 'info'); return; } var overlay = document.createElement('div'), done = 0, failed = [], stopped = false; overlay.className = 'overlay catalog-recompile-overlay'; overlay.innerHTML = '<div class="modal catalog-recompile-modal" role="dialog" aria-modal="true"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-refresh"></i></span><div><h3>Пересборка фильтров</h3><p class="text-secondary">Разделов: ' + ids.length + '</p></div><button class="modal-close" type="button" data-catalog-recompile-close aria-label="Закрыть" disabled><i class="ti ti-x"></i></button></div><div class="modal-body"><div class="catalog-recompile-status" data-catalog-recompile-status>Подготовка...</div><div class="progress"><div class="progress-bar" data-catalog-recompile-progress style="width:0%;--progress-color:var(--blue-500)"></div></div><div class="catalog-recompile-errors" data-catalog-recompile-errors></div></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-catalog-recompile-stop>Остановить</button><button class="btn btn-primary" type="button" data-catalog-recompile-close disabled>Закрыть</button></div></div>'; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); var status = overlay.querySelector('[data-catalog-recompile-status]'), bar = overlay.querySelector('[data-catalog-recompile-progress]'), errors = overlay.querySelector('[data-catalog-recompile-errors]'), close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); }, finish = function () { status.textContent = stopped ? ('Остановлено: ' + done + ' из ' + ids.length) : ('Готово: ' + done + ' из ' + ids.length); if (failed.length) { errors.innerHTML = '<div class="alert alert-error"><i class="ti ti-alert-triangle"></i><div>Ошибки: ' + failed.map(self.esc).join(', ') + '</div></div>'; } overlay.querySelectorAll('[data-catalog-recompile-close]').forEach(function (button) { button.disabled = false; }); overlay.querySelector('[data-catalog-recompile-stop]').hidden = true; }, run = function () { if (stopped || done >= ids.length) { finish(); return; } var id = ids[done], data = new FormData(); data.append('_csrf', self.csrf()); status.textContent = 'Раздел #' + id + ' · ' + (done + 1) + ' из ' + ids.length; fetch(self.url() + '/items/' + encodeURIComponent(id) + '/filters/recompile', { method: 'POST', body: data, headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(self.json).catch(function (error) { failed.push('#' + id + ': ' + ((error && error.message) || 'ошибка')); }).finally(function () { done++; bar.style.width = Math.round(done / ids.length * 100) + '%'; run(); }); }; overlay.addEventListener('click', function (e) { if (e.target.closest('[data-catalog-recompile-stop]')) { stopped = true; return; } if (e.target.closest('[data-catalog-recompile-close]') && !e.target.closest('[data-catalog-recompile-close]').disabled) { close(); } }); run(); },
scheduleItemSave: function () {
if (!this.itemForm || !this.itemForm.querySelector('[name="id"]').value) { return; }
var self = this; clearTimeout(this.itemSaveTimer); this.setState('item', 'Сохранение...');
this.itemSaveTimer = setTimeout(function () { self.saveItem(true); }, 180);
},
scheduleSettingsSave: function () {
var self = this; clearTimeout(this.settingsSaveTimer); this.setState('settings', 'Сохранение...');
this.settingsSaveTimer = setTimeout(function () { self.saveSettings(true); }, 180);
},
saveItem: function (auto) {
var id = this.itemForm.querySelector('[name="id"]').value; var self = this;
this.ajax(this.url() + '/items' + (id ? '/' + id : ''), new FormData(this.itemForm), function (payload) {
if (auto) { self.setState('item', 'Сохранено', 'ok'); self.syncTreeRow(id); if (payload.data && payload.data.condition_context) { self.renderConditionContext(payload.data.condition_context); } return; }
Adminx.Toast.show(payload.message, 'success'); if (Adminx.Drawer) { Adminx.Drawer.close('catalogItemDrawer'); } window.location.reload();
}, { quiet: !!auto, fail: function () { self.setState('item', 'Не сохранено', 'error'); } });
},
saveSettings: function (auto) { var self = this; this.ajax(this.url() + '/settings', new FormData(this.settingsForm), function (payload) { if (auto) { self.setState('settings', 'Сохранено', 'ok'); } else { self.setState('settings', 'Сохранено', 'ok'); Adminx.Toast.show(payload.message, 'success'); } }, { quiet: !!auto, fail: function () { self.setState('settings', 'Не сохранено', 'error'); } }); },
setState: function (type, message, state) { var el = document.querySelector('[data-catalog-' + type + '-state]'); if (!el) { return; } el.textContent = message || ''; el.classList.toggle('is-ok', state === 'ok'); el.classList.toggle('is-error', state === 'error'); },
syncTreeRow: function (id) { var row = document.querySelector('[data-catalog-item][data-id="' + id + '"]'); if (!row) { return; } var toggle = row.querySelector('[data-catalog-item-status]'), active = this.itemForm.querySelector('[name="status"]').checked, filterCount = this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').length; if (toggle) { toggle.checked = active; this.updateTreeStatus(toggle); } row.setAttribute('data-filter-count', String(filterCount)); var count = row.querySelector('.catalog-tree-count'); if (count) { count.textContent = this.itemForm.querySelectorAll('[name="fields_use[]"]:checked').length + ' / ' + filterCount; } },
updateTreeStatus: function (input) { var item = input.closest('[data-catalog-item]'), active = input.checked, label = item.querySelector('[data-catalog-item-status-label]'), control = input.closest('.switch'); item.classList.toggle('is-inactive', !active); if (label) { label.textContent = active ? 'активен' : 'скрыт'; } input.setAttribute('aria-label', active ? 'Скрыть раздел' : 'Включить раздел'); if (control) { control.setAttribute('data-tooltip', active ? 'Скрыть раздел' : 'Включить раздел'); } },
setTreeStatus: function (input) { var self = this, id = input.getAttribute('data-catalog-item-status'), previous = !input.checked, data = new FormData(); this.updateTreeStatus(input); input.disabled = true; data.append('_csrf', this.csrf()); data.append('status', input.checked ? '1' : '0'); this.ajax(this.url() + '/items/' + encodeURIComponent(id) + '/status', data, function (payload) { input.checked = Number(payload.data.status) === 1; input.disabled = false; self.updateTreeStatus(input); }, { quiet: true, fail: function () { input.checked = previous; input.disabled = false; self.updateTreeStatus(input); } }); },
setDocument: function (item) { if (!this.itemForm) { return; } var input = this.itemForm.querySelector('[name="document_id"]'); var label = this.itemForm.querySelector('[data-catalog-document-label]'); var clear = this.itemForm.querySelector('[data-catalog-document-clear]'); var id = item && item.id ? Number(item.id) : 0; input.value = id || ''; if (label) { label.textContent = id ? ('#' + id + ' · ' + (item.title || item.alias || 'Без названия')) : 'Документ не выбран'; } if (clear) { clear.disabled = !id; } },
openDocumentPicker: function () {
var self = this, overlay = document.createElement('div');
overlay.className = 'overlay catalog-document-overlay';
overlay.innerHTML = '<div class="modal catalog-document-modal" role="dialog" aria-modal="true"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-file-search"></i></span><div><h3>Выбрать документ</h3><p class="text-secondary">Поиск по ID, названию или alias.</p></div><button class="modal-close" type="button" data-catalog-document-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div><div class="modal-body"><label class="input-wrap"><i class="ti ti-search"></i><input class="input" type="search" placeholder="Начните вводить название или ID" data-catalog-document-search></label><div class="catalog-document-status" data-catalog-document-status>Загрузка...</div><div class="catalog-document-results" data-catalog-document-results></div></div><div class="modal-footer"><span class="mf-left text-secondary text-sm" data-catalog-document-count></span><button class="btn btn-ghost" type="button" data-catalog-document-close>Закрыть</button></div></div>';
overlay.querySelector('.catalog-document-modal').classList.add('picker-modal');
document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); });
var search = overlay.querySelector('[data-catalog-document-search]'), results = overlay.querySelector('[data-catalog-document-results]'), status = overlay.querySelector('[data-catalog-document-status]'), count = overlay.querySelector('[data-catalog-document-count]'), timer = null;
var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); };
var load = function () { status.hidden = false; status.textContent = 'Загрузка...'; fetch(self.base() + '/catalog/documents?q=' + encodeURIComponent(search.value.trim()) + '&limit=30', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(self.json).then(function (payload) { var items = (payload.data || {}).items || []; results.innerHTML = ''; items.forEach(function (item) { results.insertAdjacentHTML('beforeend', '<button class="catalog-document-result" type="button" data-document-id="' + self.esc(item.id) + '" data-document-title="' + self.esc(item.title || '') + '" data-document-alias="' + self.esc(item.alias || '') + '"><span class="mono">#' + self.esc(item.id) + '</span><span><b>' + self.esc(item.title || 'Без названия') + '</b><small>' + self.esc(item.alias || '') + '</small></span><em>' + self.esc(item.rubric_title || ('Рубрика #' + item.rubric_id)) + '</em></button>'); }); status.hidden = items.length > 0; status.textContent = 'Ничего не найдено'; count.textContent = items.length ? 'Документов: ' + items.length : ''; }).catch(function () { status.hidden = false; status.textContent = 'Не удалось загрузить документы'; }); };
overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-catalog-document-close]')) { close(); return; } var item = e.target.closest('[data-document-id]'); if (item) { self.setDocument({ id: item.getAttribute('data-document-id'), title: item.getAttribute('data-document-title'), alias: item.getAttribute('data-document-alias') }); self.scheduleItemSave(); close(); } });
search.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(load, 220); }); load(); search.focus();
},
deleteItem: function (id) {
var self = this, run = function () { var data = new FormData(); data.append('_csrf', self.csrf()); self.ajax(self.url() + '/items/' + id + '/delete', data, function (payload) { Adminx.Toast.show(payload.message, 'success'); window.location.reload(); }); };
if (Adminx.Confirm) { Adminx.Confirm.open({ kind: 'error', title: 'Удалить раздел?', message: 'Будут удалены также все вложенные разделы. Документы останутся.', confirmLabel: 'Удалить', confirmClass: 'btn-danger', onConfirm: run }); } else if (confirm('Удалить раздел и все вложенные?')) { run(); }
},
search: function (value) { value = String(value || '').trim().toLowerCase(); document.querySelectorAll('[data-catalog-item]').forEach(function (row) { row.classList.toggle('is-filtered', value && row.textContent.toLowerCase().indexOf(value) < 0); }); },
dragStart: function (e) { var handle = e.target.closest('.catalog-drag[draggable="true"]'), row = handle ? handle.closest('[data-catalog-item]') : null; if (!row) { e.preventDefault(); return; } this.dragRow = row; this.dragParent = row.parentNode; this.dragSnapshot = this.siblingOrder(this.dragParent); row.classList.add('is-dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', row.getAttribute('data-id')); },
dragOver: function (e) { if (!this.dragRow) { return; } var target = e.target.closest('[data-catalog-item]'); if (!target || target === this.dragRow || this.dragRow.contains(target)) { return; } var row = target.querySelector(':scope > .catalog-tree-row'), rect = row.getBoundingClientRect(), ratio = (e.clientY - rect.top) / rect.height, mode = ratio < .28 ? 'before' : (ratio > .72 ? 'after' : 'inside'), destination = null, reference = null, parentId = 0; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (this.dragTarget && this.dragTarget !== target) { this.dragTarget.classList.remove('is-drop-target', 'is-drop-after', 'is-drop-inside'); } this.dragTarget = target; target.classList.add('is-drop-target'); target.classList.toggle('is-drop-after', mode === 'after'); target.classList.toggle('is-drop-inside', mode === 'inside'); if (mode === 'inside') { destination = target.querySelector(':scope > .catalog-tree-children'); if (!destination) { destination = document.createElement('ol'); destination.className = 'catalog-tree-children'; target.appendChild(destination); } parentId = Number(target.getAttribute('data-id')); } else { destination = target.parentNode; reference = mode === 'before' ? target : target.nextSibling; parentId = Number(target.getAttribute('data-parent-id')); } if (destination !== this.dragRow.parentNode || (reference !== this.dragRow && this.dragRow.nextSibling !== reference)) { destination.insertBefore(this.dragRow, reference); this.dragRow.setAttribute('data-parent-id', String(parentId)); } },
dragEnd: function () { if (!this.dragRow) { return; } var changed = this.dragSnapshot !== this.siblingOrder(this.dragParent); this.dragRow.classList.remove('is-dragging'); if (this.dragTarget) { this.dragTarget.classList.remove('is-drop-target', 'is-drop-after', 'is-drop-inside'); } document.querySelectorAll('.catalog-tree-children').forEach(function (list) { if (!list.querySelector(':scope > [data-catalog-item]')) { list.remove(); } }); this.dragRow = null; this.dragParent = null; this.dragTarget = null; this.dragSnapshot = ''; if (changed) { this.saveOrder(); } },
siblingOrder: function (parent) { return Array.prototype.filter.call(parent ? parent.children : [], function (el) { return el.matches('[data-catalog-item]'); }).map(function (el) { return el.getAttribute('data-id'); }).join(','); },
saveOrder: function () { var rows = []; document.querySelectorAll('[data-catalog-item]').forEach(function (row) { var siblings = Array.prototype.filter.call(row.parentNode.children, function (el) { return el.matches('[data-catalog-item]'); }); rows.push({ id: Number(row.getAttribute('data-id')), parent_id: Number(row.getAttribute('data-parent-id')), position: siblings.indexOf(row) }); }); var data = new FormData(); data.append('_csrf', this.csrf()); data.append('order', JSON.stringify(rows)); this.ajax(this.url() + '/reorder', data, function (payload) { Adminx.Toast.show(payload.message, 'success'); }); },
ajax: function (url, data, done, options) { options = options || {}; if (!options.quiet) { Adminx.Loader.show(); } fetch(url, { method: 'POST', body: data, headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json).then(function (payload) { if (!payload.success) { throw payload; } done(payload); }).catch(function (payload) { if (options.fail) { options.fail(payload); } Adminx.Catalog.error(payload); }).finally(function () { if (!options.quiet) { Adminx.Loader.hide(); } }); },
json: function (res) { return res.json().then(function (payload) { if (!res.ok) { throw payload; } return payload; }); },
esc: function (value) { var node = document.createElement('div'); node.textContent = String(value == null ? '' : value); return node.innerHTML; },
error: function (payload) { Adminx.Toast.show((payload && payload.message) || 'Ошибка запроса', 'error'); }
};
document.addEventListener('DOMContentLoaded', function () { Adminx.Catalog.init(); });
})(window, document);