(function (window, document) { 'use strict'; var Adminx = window.Adminx || (window.Adminx = {}); Adminx.Catalog = { itemForm: null, settingsForm: null, dragItem: null, dragGroup: [], dragPlaceholder: null, dragGhost: null, dragPointerId: null, dragStartX: 0, dragStartY: 0, dragStarted: false, dragOrderSnapshot: '', orderSaving: false, 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(0); } var indent = e.target.closest('[data-catalog-item-indent]'); if (indent) { self.changeItemLevel(indent.closest('[data-catalog-item]'), parseInt(indent.getAttribute('data-catalog-item-indent'), 10) || 0); return; } var sibling = e.target.closest('[data-catalog-item-sibling]'); if (sibling) { var siblingNode = sibling.closest('[data-catalog-item]'); self.newItem(Number(siblingNode.getAttribute('data-parent-id')) || 0); return; } var child = e.target.closest('[data-catalog-item-child]'); if (child) { self.newItem(Number(child.getAttribute('data-catalog-item-child')) || 0); return; } var edit = e.target.closest('[data-catalog-item-edit]'); if (edit) { self.editItem(edit.getAttribute('data-catalog-item-edit')); return; } var del = e.target.closest('[data-catalog-item-delete]'); if (del) { self.deleteItem(del.getAttribute('data-catalog-item-delete')); return; } var treeRow = e.target.closest('[data-catalog-builder-item]'); if (treeRow && !e.target.closest('button, input, label, a, select')) { self.editItem(treeRow.getAttribute('data-catalog-builder-item')); return; } 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('pointerdown', function (e) { self.treeDragStart(e); }); document.addEventListener('pointermove', function (e) { self.treeDragOver(e); }); document.addEventListener('pointerup', function (e) { self.treeDrop(e); }); document.addEventListener('pointercancel', function () { self.treeDragEnd(); }); this.normalizeTreeHierarchy(); 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 (parentId) { this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = ''; this.itemForm.querySelector('[name="status"]').checked = true; this.itemForm.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; }); var parent = this.itemForm.querySelector('[name="parent_id"]'); if (parent) { parent.value = String(Number(parentId) || 0); } 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; this.markSelectedItem(id); 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.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; }); 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; } var treeItem = document.querySelector('[data-catalog-item][data-id="' + item.id + '"]'); this.treeBranch(treeItem).slice(1).forEach(function (child) { var option = this.itemForm.querySelector('[name="parent_id"] option[value="' + child.getAttribute('data-id') + '"]'); if (option) { option.disabled = true; } }, this); this.renderConditionContext(item.condition_context || null); document.querySelector('[data-catalog-drawer-title]').textContent = item.name || 'Раздел каталога'; this.markSelectedItem(item.id); 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 += '
' + self.esc(title) + '' + self.esc(type) + ' · #' + self.esc(id) + '
'; }); overlay.className = 'overlay catalog-filter-order-overlay'; overlay.innerHTML = ''; 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 = ''; 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 = ''; 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 = '
Ошибки: ' + failed.map(self.esc).join(', ') + '
'; } 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, name = row.querySelector('.catalog-tree-name b'); if (toggle) { toggle.checked = active; this.updateTreeStatus(toggle); } if (name) { name.textContent = this.itemForm.querySelector('[name="name"]').value || 'Без названия'; } 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 = ''; 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', ''); }); 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(); } }, markSelectedItem: function (id) { document.querySelectorAll('[data-catalog-builder-item]').forEach(function (row) { row.classList.toggle('is-selected', String(row.getAttribute('data-catalog-builder-item')) === String(id)); }); }, treeRoot: function () { return document.querySelector('[data-catalog-tree]'); }, treeNodes: function () { var root = this.treeRoot(); return root ? Array.prototype.filter.call(root.children, function (node) { return node.matches('[data-catalog-item]'); }) : []; }, search: function (value) { value = String(value || '').trim().toLowerCase(); var nodes = this.treeNodes(), byId = {}, visible = {}; nodes.forEach(function (node) { byId[String(node.getAttribute('data-id'))] = node; node.classList.add('is-filtered'); }); if (!value) { nodes.forEach(function (node) { node.classList.remove('is-filtered'); }); } else { nodes.forEach(function (node) { var name = node.querySelector('.catalog-tree-name'); if (!name || name.textContent.toLowerCase().indexOf(value) < 0) { return; } var current = node; while (current && !visible[current.getAttribute('data-id')]) { visible[current.getAttribute('data-id')] = true; current = byId[String(current.getAttribute('data-parent-id'))] || null; } }); Object.keys(visible).forEach(function (id) { if (byId[id]) { byId[id].classList.remove('is-filtered'); } }); } var root = this.treeRoot(); if (root) { root.classList.toggle('is-filtering', !!value); } }, treeDragStart: function (e) { var handle = e.target.closest('[data-catalog-drag-handle]'); var root = handle ? handle.closest('[data-catalog-tree]') : null; if (!handle || !root || handle.disabled || this.orderSaving || root.classList.contains('is-filtering') || (typeof e.button === 'number' && e.button !== 0)) { return; } this.dragItem = handle.closest('[data-catalog-item]'); if (!this.dragItem) { return; } this.dragPointerId = e.pointerId; this.dragStartX = e.clientX; this.dragStartY = e.clientY; this.dragStarted = false; this.dragOrderSnapshot = this.treeOrderSignature(); if (handle.setPointerCapture) { handle.setPointerCapture(e.pointerId); } e.preventDefault(); }, beginTreeDrag: function (e) { if (!this.dragItem || this.dragStarted) { return; } var row = this.dragItem.querySelector('[data-catalog-builder-item]'); var firstRect = this.dragItem.getBoundingClientRect(); var rowRect = row.getBoundingClientRect(); this.dragStarted = true; this.dragGroup = this.treeBranch(this.dragItem); this.dragPlaceholder = document.createElement('li'); this.dragPlaceholder.className = 'catalog-tree-placeholder'; this.dragPlaceholder.setAttribute('aria-hidden', 'true'); var lastRect = this.dragGroup[this.dragGroup.length - 1].getBoundingClientRect(); this.dragPlaceholder.style.height = Math.max(52, lastRect.bottom - firstRect.top) + 'px'; this.dragItem.parentNode.insertBefore(this.dragPlaceholder, this.dragItem); this.dragGhost = row.cloneNode(true); this.dragGhost.classList.remove('is-selected'); this.dragGhost.classList.add('catalog-tree-ghost'); this.dragGhost.style.width = Math.min(rowRect.width, 520, Math.max(1, window.innerWidth - 24)) + 'px'; document.body.appendChild(this.dragGhost); this.positionTreeDragGhost(e); document.body.classList.add('catalog-tree-dragging'); this.treeRoot().classList.add('is-drag-active'); this.dragGroup.forEach(function (node, index) { node.classList.add(index === 0 ? 'is-dragging' : 'is-dragging-child'); }); }, treeDragOver: function (e) { if (!this.dragItem || e.pointerId !== this.dragPointerId) { return; } if (!this.dragStarted) { if (Math.abs(e.clientX - this.dragStartX) < 5 && Math.abs(e.clientY - this.dragStartY) < 5) { return; } this.beginTreeDrag(e); } e.preventDefault(); this.positionTreeDragGhost(e); this.scrollTree(e.clientY); var target = null, bottom = false, self = this; this.treeNodes().some(function (node) { if (self.dragGroup.indexOf(node) !== -1 || node.classList.contains('is-filtered')) { return false; } var candidate = node.querySelector('[data-catalog-builder-item]'); var rect = candidate ? candidate.getBoundingClientRect() : null; if (!rect || e.clientY >= rect.bottom) { return false; } target = node; bottom = e.clientY > rect.top + rect.height / 2; return true; }); document.querySelectorAll('.catalog-tree-row.drag-over-top, .catalog-tree-row.drag-over-bottom').forEach(function (row) { row.classList.remove('drag-over-top', 'drag-over-bottom'); }); if (!target) { this.treeRoot().appendChild(this.dragPlaceholder); return; } var targetRow = target.querySelector('[data-catalog-builder-item]'); var branch = this.treeBranch(target); var anchor = bottom ? branch[branch.length - 1].nextSibling : target; if (anchor !== this.dragPlaceholder) { target.parentNode.insertBefore(this.dragPlaceholder, anchor); } targetRow.classList.toggle('drag-over-top', !bottom); targetRow.classList.toggle('drag-over-bottom', bottom); }, treeDrop: function (e) { if (!this.dragItem || e.pointerId !== this.dragPointerId) { return; } e.preventDefault(); if (!this.dragStarted || !this.dragPlaceholder || !this.dragPlaceholder.parentNode) { this.treeDragEnd(); return; } var placeholder = this.dragPlaceholder; this.dragGroup.forEach(function (node) { placeholder.parentNode.insertBefore(node, placeholder); }); placeholder.parentNode.removeChild(placeholder); this.dragPlaceholder = null; this.normalizeTreeHierarchy(); var changed = this.treeOrderSignature() !== this.dragOrderSnapshot; this.treeDragEnd(); if (changed) { this.persistTreeOrder(); } }, treeDragEnd: function () { this.dragGroup.forEach(function (node) { node.classList.remove('is-dragging', 'is-dragging-child'); }); if (this.dragPlaceholder && this.dragPlaceholder.parentNode) { this.dragPlaceholder.parentNode.removeChild(this.dragPlaceholder); } document.querySelectorAll('.catalog-tree-row.drag-over-top, .catalog-tree-row.drag-over-bottom').forEach(function (row) { row.classList.remove('drag-over-top', 'drag-over-bottom'); }); var root = this.treeRoot(); if (root) { root.classList.remove('is-drag-active'); } if (this.dragGhost && this.dragGhost.parentNode) { this.dragGhost.parentNode.removeChild(this.dragGhost); } document.body.classList.remove('catalog-tree-dragging'); this.dragItem = null; this.dragGroup = []; this.dragPlaceholder = null; this.dragGhost = null; this.dragPointerId = null; this.dragStarted = false; this.dragOrderSnapshot = ''; }, positionTreeDragGhost: function (e) { if (!this.dragGhost) { return; } var left = Math.min(e.clientX + 14, window.innerWidth - this.dragGhost.offsetWidth - 12); var top = Math.min(e.clientY + 12, window.innerHeight - this.dragGhost.offsetHeight - 12); this.dragGhost.style.transform = 'translate3d(' + Math.max(12, left) + 'px,' + Math.max(12, top) + 'px,0)'; }, scrollTree: function (clientY) { var edge = 72; if (clientY < edge) { window.scrollBy(0, -Math.ceil((edge - clientY) / 5)); } if (clientY > window.innerHeight - edge) { window.scrollBy(0, Math.ceil((clientY - window.innerHeight + edge) / 5)); } }, treeOrderSignature: function () { return this.treeNodes().map(function (node) { return node.getAttribute('data-id') + ':' + node.getAttribute('data-level'); }).join('|'); }, treeBranch: function (node) { if (!node) { return []; } var branch = [node], level = parseInt(node.getAttribute('data-level'), 10) || 0, next = node.nextElementSibling; while (next) { if (next === this.dragPlaceholder) { next = next.nextElementSibling; continue; } if (!next.matches('[data-catalog-item]') || (parseInt(next.getAttribute('data-level'), 10) || 0) <= level) { break; } branch.push(next); next = next.nextElementSibling; } return branch; }, canIndentItem: function (node) { var level = parseInt(node.getAttribute('data-level'), 10) || 0, previous = node.previousElementSibling; while (previous) { if (!previous.matches('[data-catalog-item]')) { previous = previous.previousElementSibling; continue; } var previousLevel = parseInt(previous.getAttribute('data-level'), 10) || 0; if (previousLevel < level) { return false; } if (previousLevel === level) { return true; } previous = previous.previousElementSibling; } return false; }, changeItemLevel: function (node, delta) { if (!node || !delta || this.orderSaving) { return; } var root = this.treeRoot(); if (root && root.classList.contains('is-filtering')) { Adminx.Toast.show('Очистите поиск перед изменением структуры', 'info'); return; } var level = parseInt(node.getAttribute('data-level'), 10) || 0; if (delta > 0 && !this.canIndentItem(node)) { return; } if (delta < 0 && level === 0) { return; } var shift = delta > 0 ? 1 : -1; this.treeBranch(node).forEach(function (branchNode) { var branchLevel = parseInt(branchNode.getAttribute('data-level'), 10) || 0; branchNode.setAttribute('data-level', String(Math.max(0, branchLevel + shift))); }); this.normalizeTreeHierarchy(); this.persistTreeOrder(); }, normalizeTreeHierarchy: function () { var nodes = this.treeNodes(), parents = [], positions = {}, previousLevel = 0, self = this; nodes.forEach(function (node, index) { var level = Math.max(0, parseInt(node.getAttribute('data-level'), 10) || 0); if (index === 0) { level = 0; } if (level > previousLevel + 1) { level = previousLevel + 1; } var parentId = level > 0 && parents[level - 1] ? parents[level - 1] : 0; if (level > 0 && !parentId) { level = 0; parentId = 0; } var id = parseInt(node.getAttribute('data-id'), 10) || 0; var key = String(parentId); var position = positions[key] || 0; positions[key] = position + 1; parents[level] = id; parents.length = level + 1; previousLevel = level; node.setAttribute('data-level', String(level)); node.setAttribute('data-parent-id', String(parentId)); node.setAttribute('data-position', String(position)); node.style.setProperty('--catalog-indent', Math.min(level, 10) * 28 + 'px'); var number = node.querySelector('.catalog-tree-position'); if (number) { number.textContent = String(position + 1); } var badge = node.querySelector('[data-catalog-level-badge]'); if (badge) { badge.textContent = 'уровень ' + (level + 1); } }); nodes.forEach(function (node) { var level = parseInt(node.getAttribute('data-level'), 10) || 0; var decrease = node.querySelector('[data-catalog-item-indent="-1"]'); var increase = node.querySelector('[data-catalog-item-indent="1"]'); if (decrease) { decrease.disabled = level === 0; } if (increase) { increase.disabled = !self.canIndentItem(node); } }); }, persistTreeOrder: function () { if (this.orderSaving) { return; } this.normalizeTreeHierarchy(); var rows = this.treeNodes().map(function (node) { return { id: Number(node.getAttribute('data-id')), parent_id: Number(node.getAttribute('data-parent-id')), position: Number(node.getAttribute('data-position')) }; }); var data = new FormData(), self = this, root = this.treeRoot(); data.append('_csrf', this.csrf()); data.append('order', JSON.stringify(rows)); this.orderSaving = true; if (root) { root.classList.add('is-order-saving'); } this.ajax(this.url() + '/reorder', data, function (payload) { self.orderSaving = false; if (root) { root.classList.remove('is-order-saving'); } Adminx.Toast.show(payload.message || 'Порядок сохранён', 'success'); }, { quiet: true, fail: function () { self.orderSaving = false; if (root) { root.classList.remove('is-order-saving'); } setTimeout(function () { window.location.reload(); }, 400); } }); }, 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);