/** * JS раздела «Навигация»: drawer-CRUD, импорт legacy, фильтры и копирование тегов. */ (function (window, document) { 'use strict'; var Adminx = window.Adminx || (window.Adminx = {}); Adminx.Navigation = { form: null, itemForm: null, filterTimer: null, aliasTimer: null, aliasState: 'empty', currentNavigationId: 0, currentNavigationFlat: [], currentItemId: 0, pendingItemId: 0, dragItem: null, dragGroup: [], dragPlaceholder: null, dragGhost: null, dragPointerId: null, dragStartX: 0, dragStartY: 0, dragStarted: false, dragOrderSnapshot: '', orderSaving: false, activeTemplateEditor: null, init: function () { this.form = document.getElementById('navigationForm'); this.itemForm = document.getElementById('navigationItemForm'); var self = this; document.addEventListener('click', function (e) { if (e.target.closest('[data-navigation-new]')) { self.fillNew(); } if (e.target.closest('[data-navigation-filter-reset]')) { self.resetFilters(); } var edit = e.target.closest('[data-navigation-edit]'); if (edit) { self.fillEdit(edit.closest('[data-navigation-row]')); } var items = e.target.closest('[data-navigation-items]'); if (items) { self.openItems(items.closest('[data-navigation-row]')); } if (e.target.closest('[data-navigation-item-new]')) { self.fillItemNew(); } if (e.target.closest('[data-navigation-item-reset]')) { self.reloadCurrentItem(); } var itemEdit = e.target.closest('[data-navigation-item-edit]'); if (itemEdit) { self.fillItemEdit(itemEdit.getAttribute('data-navigation-item-edit')); } var itemPick = e.target.closest('[data-navigation-builder-item]'); if (itemPick && !e.target.closest('button, .navigation-drag-handle')) { self.fillItemEdit(itemPick.getAttribute('data-navigation-builder-item')); } var itemToggle = e.target.closest('[data-navigation-item-toggle]'); if (itemToggle) { self.toggleItem(itemToggle.getAttribute('data-navigation-item-toggle')); } var itemDelete = e.target.closest('[data-navigation-item-delete]'); if (itemDelete) { self.deleteItem(itemDelete.getAttribute('data-navigation-item-delete')); } var itemIndent = e.target.closest('[data-navigation-item-indent]'); if (itemIndent) { self.changeItemLevel(itemIndent.closest('[data-navigation-item-node]'), parseInt(itemIndent.getAttribute('data-navigation-item-indent'), 10) || 0); } if (e.target.closest('[data-navigation-document-pick]')) { self.openDocumentPicker(); } if (e.target.closest('[data-navigation-document-clear]')) { self.clearPickedDocument(); } if (e.target.closest('[data-navigation-image-pick]')) { self.openImagePicker(); } if (e.target.closest('[data-navigation-image-clear]')) { self.applyPickedImage(''); } var tagsToggle = e.target.closest('[data-navigation-tags-toggle]'); if (tagsToggle) { e.preventDefault(); self.openTagPalette(tagsToggle.closest('.navigation-code-field')); } var templateTab = e.target.closest('[data-navigation-template-tab]'); if (templateTab) { self.activateTemplateSection(templateTab.getAttribute('data-navigation-template-tab')); } if (e.target.closest('[data-navigation-tags-close]')) { self.closeTagPalette(); } var tagTab = e.target.closest('[data-navigation-tag-tab]'); if (tagTab) { self.activateTagGroup(tagTab.getAttribute('data-navigation-tag-tab')); } var templateTag = e.target.closest('[data-navigation-template-tag]'); if (templateTag) { self.insertTemplateTag(templateTag); } var copy = e.target.closest('[data-navigation-copy]'); if (copy) { self.copy(copy.closest('[data-navigation-row]')); } var cache = e.target.closest('[data-navigation-cache]'); if (cache) { self.clearCache(cache.closest('[data-navigation-row]')); } var del = e.target.closest('[data-navigation-delete]'); if (del) { self.remove(del.closest('[data-navigation-row]')); } if (e.target.closest('[data-navigation-submit-stay]')) { self.submit(true); } }); if (this.form) { this.form.addEventListener('submit', function (e) { e.preventDefault(); self.submit(false); }); } if (this.itemForm) { this.itemForm.addEventListener('submit', function (e) { e.preventDefault(); self.submitItem(); }); this.itemForm.addEventListener('input', function (e) { if (e.target && e.target.matches('[data-navigation-image-input]')) { self.renderPickedImage(e.target.value); } }); } this.openDeepLink(); document.addEventListener('submit', function (e) { var filter = e.target.closest('.navigation-filter'); if (!filter) { return; } e.preventDefault(); self.applyFilters(filter, true); }); document.addEventListener('input', function (e) { if (e.target.matches('[data-navigation-tags-search]')) { self.filterTags(e.target.value); return; } var filter = e.target.closest('.navigation-filter'); if (!filter || !e.target.matches('input[type="search"]')) { return; } clearTimeout(self.filterTimer); self.filterTimer = setTimeout(function () { self.applyFilters(filter, true); }, 350); }); document.addEventListener('focusin', function (e) { if (e.target && e.target.matches && e.target.matches('#navigationForm textarea[data-code-editor]')) { self.activeTemplateEditor = e.target._adminxCodeMirror || null; } }); document.addEventListener('input', function (e) { if (!self.form || e.target !== self.field('alias')) { return; } self.scheduleAliasCheck(); }); document.addEventListener('change', function (e) { if (e.target && e.target.matches('[data-navigation-group-option]')) { self.syncGroupAccess(); } }); document.addEventListener('click', function (e) { if (e.target.closest('[data-navigation-groups-all]')) { self.selectAllGroups(true); } else if (e.target.closest('[data-navigation-groups-none]')) { self.selectAllGroups(false); } }); document.addEventListener('blur', function (e) { if (!self.form || e.target !== self.field('alias')) { return; } self.checkAlias(true); }, true); window.addEventListener('popstate', function () { self.applyFilterUrl(window.location.href, false); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && self.isTagPaletteOpen()) { self.closeTagPalette(); return; } if (e.target && e.target.closest && e.target.closest('.CodeMirror')) { return; } if ((e.ctrlKey || e.metaKey) && String(e.key || '').toLowerCase() === 's' && self.isDrawerOpen()) { e.preventDefault(); self.submit(true); } }); document.addEventListener('pointerdown', function (e) { self.itemDragStart(e); }); document.addEventListener('pointermove', function (e) { self.itemDragOver(e); }); document.addEventListener('pointerup', function (e) { self.itemDrop(e); }); document.addEventListener('pointercancel', function () { self.itemDragEnd(); }); this.openFromLocation(); }, openFromLocation: function () { var id = parseInt(new URLSearchParams(window.location.search).get('edit'), 10) || 0; if (!id) { return; } var row = document.querySelector('[data-navigation-row][data-id="' + id + '"]'); if (!row) { return; } if (Adminx.Drawer) { Adminx.Drawer.open('navigationDrawer'); } this.fillEdit(row); }, base: function () { return (this.form && this.form.getAttribute('data-base')) || Adminx.base(); }, field: function (name) { return this.form.querySelector('[name="' + name + '"]'); }, itemField: function (name) { return this.itemForm.querySelector('[name="' + name + '"]'); }, filterUrl: function (form) { var params = new URLSearchParams(new FormData(form)); Array.from(params.keys()).forEach(function (key) { if (String(params.get(key) || '') === '') { params.delete(key); } }); var query = params.toString(); return (form.getAttribute('action') || (this.base() + '/navigation')) + (query ? '?' + query : ''); }, applyFilters: function (form, push) { if (!form) { return; } this.applyFilterUrl(this.filterUrl(form), push); }, applyFilterUrl: function (url, push) { var self = this; Adminx.Loader.show(); fetch(url, { method: 'GET', headers: { 'Accept': 'text/html' }, credentials: 'same-origin' }) .then(function (res) { return res.text().then(function (html) { if (!res.ok) { throw new Error('HTTP ' + res.status); } self.replaceList(html, url, push); }); }) .catch(function () { Adminx.Toast.show('Не удалось применить фильтры', 'error'); }) .finally(function () { Adminx.Loader.hide(); }); }, replaceList: function (html, url, push) { var doc = new DOMParser().parseFromString(html, 'text/html'); ['.navigation-summary', '.navigation-import-card', '.navigation-panel'].forEach(function (selector) { var next = doc.querySelector(selector); var current = document.querySelector(selector); if (next && current) { current.replaceWith(next); } }); if (push && window.history && window.history.pushState) { window.history.pushState({ adminxNavigationFilters: true }, '', url); } }, resetFilters: function () { var form = document.querySelector('.navigation-filter'); if (!form) { return; } form.reset(); this.applyFilters(form, true); }, isDrawerOpen: function () { var drawer = document.getElementById('navigationDrawer'); return !!(drawer && !drawer.hidden && this.form); }, fillNew: function () { if (!this.form) { return; } this.closeTagPalette(); this.form.reset(); this.field('id').value = ''; this.field('title').value = ''; this.field('alias').value = ''; this.field('expand_ext').value = '1'; this.selectAllGroups(true); var templates = { begin: '', level1_begin: '' }; Object.keys(templates).forEach(function (key) { this.field(key).value = templates[key]; }, this); this.activateTemplateSection('base'); this.syncEditors(); this.clearErrors(); this.setAliasState('', ''); document.getElementById('navigationDrawerTitle').textContent = 'Новая навигация'; setTimeout(function () { if (Adminx.CodeEditor) { Adminx.CodeEditor.refreshAll(); } }, 120); }, fillEdit: function (row) { if (!row) { return; } this.closeTagPalette(); var self = this; var id = row.getAttribute('data-id'); Adminx.Loader.show(); fetch(this.base() + '/navigation/' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(this.json) .then(function (payload) { var item = payload.data || {}; self.form.reset(); Object.keys(item).forEach(function (key) { var field = self.field(key); if (field) { field.value = item[key] == null ? '' : item[key]; } }); self.field('id').value = item.navigation_id || ''; self.setGroupAccess(item.user_group || ''); document.getElementById('navigationDrawerTitle').textContent = item.title || 'Навигация'; self.clearErrors(); self.setAliasState('ok', item.alias ? 'Алиас сохранён' : ''); self.activateTemplateSection('base'); self.syncEditors(); setTimeout(function () { if (Adminx.CodeEditor) { Adminx.CodeEditor.refreshAll(); } }, 120); }) .catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось загрузить навигацию', 'error'); }) .finally(function () { Adminx.Loader.hide(); }); }, setGroupAccess: function (value) { var selected = String(value || '').split(',').map(function (id) { return String(parseInt(id, 10) || ''); }); document.querySelectorAll('[data-navigation-group-option]').forEach(function (option) { option.checked = selected.indexOf(String(option.value)) !== -1; }); this.syncGroupAccess(); }, selectAllGroups: function (checked) { document.querySelectorAll('[data-navigation-group-option]').forEach(function (option) { option.checked = !!checked; }); this.syncGroupAccess(); }, syncGroupAccess: function () { if (!this.form) { return; } var options = Array.prototype.slice.call(document.querySelectorAll('[data-navigation-group-option]')); var selected = options.filter(function (option) { return option.checked; }); var input = this.field('user_group'); var summary = document.querySelector('[data-navigation-groups-summary]'); var detail = document.querySelector('[data-navigation-groups-detail]'); var count = document.querySelector('[data-navigation-groups-count]'); if (input) { input.value = selected.map(function (option) { return option.value; }).join(','); } if (count) { count.textContent = selected.length + ' из ' + options.length; } if (!summary || !detail) { return; } if (!selected.length) { summary.textContent = 'Никому не доступна'; detail.textContent = 'Выберите хотя бы одну группу'; } else if (selected.length === options.length) { summary.textContent = 'Все группы'; detail.textContent = selected.map(function (option) { return option.getAttribute('data-group-name'); }).join(', '); } else if (selected.length === 1) { summary.textContent = selected[0].getAttribute('data-group-name') || ('Группа #' + selected[0].value); detail.textContent = 'Выбрана 1 группа'; } else { summary.textContent = 'Выбрано групп: ' + selected.length; detail.textContent = selected.map(function (option) { return option.getAttribute('data-group-name'); }).join(', '); } }, syncEditors: function () { var self = this; Array.prototype.forEach.call(this.form.querySelectorAll('textarea[data-code-editor]'), function (textarea) { if (textarea._adminxCodeMirror) { textarea._adminxCodeMirror.setValue(textarea.value || ''); textarea._adminxCodeMirror.on('focus', function (cm) { self.activeTemplateEditor = cm; }); setTimeout(function () { textarea._adminxCodeMirror.refresh(); }, 40); } }); }, activateTemplateSection: function (name) { var panel = null; name = String(name || 'base'); this.closeTagPalette(); document.querySelectorAll('[data-navigation-template-tab]').forEach(function (tab) { var active = tab.getAttribute('data-navigation-template-tab') === name; tab.classList.toggle('is-active', active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); }); document.querySelectorAll('[data-navigation-template-panel]').forEach(function (item) { var active = item.getAttribute('data-navigation-template-panel') === name; item.hidden = !active; if (active) { panel = item; } }); if (panel) { setTimeout(function () { panel.querySelectorAll('textarea[data-code-editor]').forEach(function (textarea) { if (textarea._adminxCodeMirror) { textarea._adminxCodeMirror.refresh(); } }); }, 30); } }, isTagPaletteOpen: function () { var palette = document.querySelector('[data-navigation-tags]'); return !!(palette && !palette.hidden); }, openTagPalette: function (field) { var palette = document.querySelector('[data-navigation-tags]'); var textarea = field ? field.querySelector('textarea[data-code-editor]') : null; var anchor = textarea && textarea._adminxCodeMirror ? textarea._adminxCodeMirror.getWrapperElement() : textarea; var search; if (!palette || !field || !textarea) { return; } document.querySelectorAll('.navigation-code-field.is-tags-open').forEach(function (item) { item.classList.remove('is-tags-open'); }); field.classList.add('is-tags-open'); this.activeTemplateEditor = textarea._adminxCodeMirror || this.activeTemplateEditor; field.insertBefore(palette, anchor || field.firstChild); palette.hidden = false; search = palette.querySelector('[data-navigation-tags-search]'); if (search) { search.value = ''; this.filterTags(''); search.focus(); } }, closeTagPalette: function () { var palette = document.querySelector('[data-navigation-tags]'); if (palette) { palette.hidden = true; } document.querySelectorAll('.navigation-code-field.is-tags-open').forEach(function (item) { item.classList.remove('is-tags-open'); }); }, activateTagGroup: function (name) { var palette = document.querySelector('[data-navigation-tags]'); if (!palette) { return; } palette.querySelectorAll('[data-navigation-tag-tab]').forEach(function (tab) { tab.classList.toggle('is-active', tab.getAttribute('data-navigation-tag-tab') === String(name)); }); palette.querySelectorAll('[data-navigation-tag-panel]').forEach(function (group) { group.classList.toggle('is-active', group.getAttribute('data-navigation-tag-panel') === String(name)); }); }, filterTags: function (query) { var palette = document.querySelector('[data-navigation-tags]'); var shown = 0; if (!palette) { return; } query = String(query || '').trim().toLowerCase(); palette.classList.toggle('is-searching', query !== ''); palette.querySelectorAll('[data-navigation-template-tag]').forEach(function (button) { var visible = query === '' || button.textContent.toLowerCase().indexOf(query) !== -1; button.hidden = !visible; if (visible) { shown++; } }); palette.querySelectorAll('[data-navigation-tag-group]').forEach(function (group) { group.hidden = query !== '' && !group.querySelector('[data-navigation-template-tag]:not([hidden])'); }); var empty = palette.querySelector('[data-navigation-tags-empty]'); if (empty) { empty.hidden = shown > 0; } }, insertTemplateTag: function (button) { var value = button.getAttribute('data-navigation-template-tag') || ''; var select = button.getAttribute('data-navigation-template-select') || value; var editor = this.activeTemplateEditor || this.editorFallback(); if (!value || !editor) { return; } editor.replaceSelection(value); var cursor = editor.getCursor(); if (select && value.indexOf(select) !== -1) { var from = editor.indexFromPos(cursor) - value.length + value.indexOf(select); editor.setSelection(editor.posFromIndex(from), editor.posFromIndex(from + select.length)); } editor.focus(); editor.save(); this.closeTagPalette(); }, editorFallback: function () { var textarea = this.form ? this.form.querySelector('textarea[data-code-editor]') : null; return textarea && textarea._adminxCodeMirror ? textarea._adminxCodeMirror : null; }, saveEditors: function () { Array.prototype.forEach.call(this.form.querySelectorAll('textarea[data-code-editor]'), function (textarea) { if (textarea._adminxCodeMirror) { textarea._adminxCodeMirror.save(); } }); }, scheduleAliasCheck: function () { var self = this; clearTimeout(this.aliasTimer); this.setAliasState('pending', 'Проверяем алиас...'); this.aliasTimer = setTimeout(function () { self.checkAlias(false); }, 350); }, checkAlias: function (force) { if (!this.form) { return Promise.resolve(false); } var alias = this.field('alias').value || ''; var id = this.field('id').value || '0'; if (!alias && !force) { this.setAliasState('empty', ''); return Promise.resolve(false); } var self = this; return fetch(this.base() + '/navigation/alias-check?alias=' + encodeURIComponent(alias) + '&id=' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json).then(function (payload) { var data = payload.data || {}; self.setAliasState(data.valid && data.available ? 'ok' : 'error', payload.message || ''); return !!(data.valid && data.available); }).catch(function () { self.setAliasState('error', 'Не удалось проверить алиас'); return false; }); }, setAliasState: function (state, message) { this.aliasState = state || 'empty'; var node = document.querySelector('[data-navigation-alias-state]'); var field = this.form ? this.field('alias') : null; if (node) { node.textContent = message || ''; node.className = 'field-hint navigation-alias-state is-' + this.aliasState; } if (field) { field.classList.toggle('is-invalid', this.aliasState === 'error'); } }, submit: function (stay) { if (!this.form) { return; } var self = this; this.saveEditors(); this.clearErrors(); this.checkAlias(true).then(function (ok) { if (!ok) { Adminx.Toast.show('Проверьте алиас перед сохранением', 'error'); return; } var id = self.field('id').value; var url = id ? self.base() + '/navigation/' + encodeURIComponent(id) : self.base() + '/navigation'; self.post(url, new FormData(self.form)) .then(function (payload) { if (stay && payload.data && payload.data.id) { self.field('id').value = payload.data.id; } self.ajaxRefresh(payload.message || 'Сохранено', stay); }) .catch(function (err) { self.showErrors(err); }); }); }, copy: function (row) { if (!row) { return; } var id = row.getAttribute('data-id'); var data = new FormData(); data.append('_csrf', this.csrf()); data.append('title', (row.getAttribute('data-title') || 'Навигация') + ' (копия)'); var self = this; this.post(this.base() + '/navigation/' + encodeURIComponent(id) + '/copy', data) .then(function (payload) { self.ajaxRefresh(payload.message || 'Копия создана'); }) .catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось создать копию', 'error'); }); }, clearCache: function (row) { if (!row) { return; } var data = new FormData(); data.append('_csrf', this.csrf()); var self = this; this.post(this.base() + '/navigation/' + encodeURIComponent(row.getAttribute('data-id')) + '/clear-cache', data) .then(function (payload) { self.ajaxRefresh(payload.message || 'Кеш очищен', true); }) .catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось очистить кеш', 'error'); }); }, remove: function (row) { if (!row) { return; } var data = new FormData(); data.append('_csrf', this.csrf()); var self = this; var run = function () { self.post(self.base() + '/navigation/' + encodeURIComponent(row.getAttribute('data-id')) + '/delete', data) .then(function (payload) { self.ajaxRefresh(payload.message || 'Удалено'); }) .catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось удалить', 'error'); }); }; if (Adminx.Confirm) { Adminx.Confirm.open({ kind: 'danger', title: 'Удалить навигацию', message: 'Будут удалены шаблон меню и все его пункты.', confirmLabel: 'Удалить', onConfirm: run }); } else { run(); } }, openItems: function (row) { if (!row) { return; } this.currentNavigationId = parseInt(row.getAttribute('data-id'), 10) || 0; this.currentItemId = 0; this.setItemFormMode('empty'); this.loadItems(); }, openDeepLink: function () { var params = new URLSearchParams(window.location.search || ''); var navigationId = parseInt(params.get('navigation'), 10) || 0; var itemId = parseInt(params.get('item'), 10) || 0; if (!navigationId) { return; } var row = document.querySelector('[data-navigation-row][data-id="' + navigationId + '"]'); if (!row) { return; } this.pendingItemId = itemId; this.openItems(row); if (Adminx.Drawer) { Adminx.Drawer.open('navigationItemsDrawer'); } }, loadItems: function () { var self = this; if (!this.currentNavigationId) { return; } Adminx.Loader.show(); fetch(this.base() + '/navigation/' + encodeURIComponent(this.currentNavigationId) + '/items', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json).then(function (payload) { var data = payload.data || {}; self.currentNavigationFlat = data.flat || []; var title = document.getElementById('navigationItemsDrawerTitle'); var subtitle = document.querySelector('[data-navigation-items-subtitle]'); var count = document.querySelector('[data-navigation-items-count]'); if (title) { title.textContent = data.navigation ? data.navigation.title : 'Пункты навигации'; } if (subtitle) { subtitle.textContent = data.navigation ? data.navigation.tag : 'Структура меню'; } if (count) { count.textContent = String(self.currentNavigationFlat.length); } self.renderItems(data.items || []); if (self.pendingItemId) { var pendingItemId = self.pendingItemId; self.pendingItemId = 0; self.fillItemEdit(pendingItemId); } if (self.currentItemId) { self.markSelectedItem(self.currentItemId); } }).catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось загрузить пункты', 'error'); }).finally(function () { Adminx.Loader.hide(); }); }, renderItems: function (items) { var root = document.querySelector('[data-navigation-items-tree]'); if (!root) { return; } if (!items.length) { root.innerHTML = '
Пункты не найдены.
'; return; } var flat = []; this.flattenItems(items, flat); root.innerHTML = '
    ' + flat.map(this.itemHtml.bind(this)).join('') + '
'; this.normalizeItemHierarchy(); }, flattenItems: function (items, out) { items.forEach(function (item) { out.push(item); if (item.children && item.children.length) { this.flattenItems(item.children, out); } }, this); }, itemHtml: function (item) { var level = Math.max(0, Math.min(2, (parseInt(item.level, 10) || 1) - 1)); var status = String(item.status) === '1' ? 'активен' : 'выкл'; return '
  • '; }, fillItemNew: function () { if (!this.itemForm) { return; } this.itemForm.reset(); this.currentItemId = 0; this.itemField('navigation_id').value = this.currentNavigationId || ''; this.itemField('navigation_item_id').value = ''; this.itemField('target').value = '_self'; this.renderPickedDocument(null); this.applyPickedImage(''); this.populateParents(0); this.clearItemErrors(); this.setItemFormMode('new', 'Новый пункт меню'); this.markSelectedItem(0); var title = this.itemField('title'); if (title) { setTimeout(function () { title.focus(); }, 40); } }, fillItemEdit: function (id) { var self = this; fetch(this.base() + '/navigation/items/' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json).then(function (payload) { var item = payload.data || {}; self.itemForm.reset(); Object.keys(item).forEach(function (key) { var field = self.itemField(key); if (field) { field.value = item[key] == null ? '' : item[key]; } }); self.currentItemId = parseInt(item.navigation_item_id, 10) || 0; self.itemField('navigation_item_id').value = item.navigation_item_id || ''; self.itemField('navigation_id').value = item.navigation_id || self.currentNavigationId || ''; self.populateParents(item.parent_id || 0, item.navigation_item_id || 0); self.renderPickedDocument(item); self.applyPickedImage(item.image || ''); self.clearItemErrors(); self.setItemFormMode('edit', item.title || 'Пункт меню'); self.markSelectedItem(self.currentItemId); }).catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось загрузить пункт', 'error'); }); }, reloadCurrentItem: function () { if (this.currentItemId) { this.fillItemEdit(this.currentItemId); } else { this.fillItemNew(); } }, setItemFormMode: function (mode, title) { var empty = document.querySelector('[data-navigation-item-empty]'); var fields = document.querySelector('[data-navigation-item-fields]'); var state = document.querySelector('[data-navigation-item-form-state]'); var titleNode = document.querySelector('[data-navigation-item-form-title]'); var save = document.querySelector('[data-navigation-item-save]'); var reset = document.querySelector('[data-navigation-item-reset]'); var active = mode === 'new' || mode === 'edit'; if (empty) { empty.hidden = active; } if (fields) { fields.hidden = !active; } if (titleNode) { titleNode.textContent = title || 'Свойства пункта'; } if (state) { state.textContent = mode === 'new' ? 'новый' : (mode === 'edit' ? 'редактирование' : 'не выбран'); state.className = 'badge ' + (mode === 'new' ? 'badge-green' : (mode === 'edit' ? 'badge-blue' : 'badge-gray')); } if (save) { save.disabled = !active; } if (reset) { reset.disabled = !active; } }, markSelectedItem: function (id) { document.querySelectorAll('[data-navigation-builder-item]').forEach(function (node) { node.classList.toggle('is-selected', String(node.getAttribute('data-navigation-builder-item')) === String(id)); }); }, populateParents: function (selected, exclude) { var select = this.itemField('parent_id'); if (!select) { return; } var html = ''; this.currentNavigationFlat.forEach(function (item) { if (String(item.navigation_item_id) === String(exclude) || parseInt(item.level, 10) >= 3 || this.itemIsDescendant(item.navigation_item_id, exclude)) { return; } html += ''; }, this); select.innerHTML = html; select.value = String(selected || 0); }, itemIsDescendant: function (candidateId, ancestorId) { candidateId = parseInt(candidateId, 10) || 0; ancestorId = parseInt(ancestorId, 10) || 0; if (!candidateId || !ancestorId) { return false; } var byId = {}; this.currentNavigationFlat.forEach(function (item) { byId[parseInt(item.navigation_item_id, 10) || 0] = item; }); var cursor = candidateId; var seen = {}; while (cursor && byId[cursor] && !seen[cursor]) { if (parseInt(byId[cursor].parent_id, 10) === ancestorId) { return true; } seen[cursor] = true; cursor = parseInt(byId[cursor].parent_id, 10) || 0; } return false; }, openDocumentPicker: function () { var self = this; var overlay = document.createElement('div'); overlay.className = 'overlay navigation-picker-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); this.bindDocumentPicker(overlay); }, bindDocumentPicker: function (overlay) { var self = this; var list = overlay.querySelector('[data-navigation-document-list]'); var status = overlay.querySelector('[data-navigation-document-status]'); var search = overlay.querySelector('[data-navigation-document-search]'); var count = overlay.querySelector('[data-navigation-document-count]'); var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); document.removeEventListener('keydown', onKey); }, 160); }; var render = function (items) { list.innerHTML = ''; (items || []).forEach(function (item) { list.insertAdjacentHTML('beforeend', ''); }); status.hidden = items && items.length > 0; status.textContent = search.value.trim() ? 'Ничего не найдено' : 'Документы не найдены'; count.textContent = items && items.length ? 'Документов: ' + items.length : ''; }; var load = function () { var params = new URLSearchParams(); params.set('q', search.value.trim()); params.set('limit', 30); status.textContent = 'Загрузка...'; status.hidden = false; fetch(self.base() + '/navigation/documents/picker?' + params.toString(), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(self.json) .then(function (payload) { render((payload.data || {}).items || []); }) .catch(function () { list.innerHTML = ''; status.textContent = 'Не удалось загрузить документы'; status.hidden = false; }); }; var timer = null; var onKey = function (e) { if (e.key === 'Escape') { close(); } }; overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-navigation-picker-close]')) { close(); return; } var item = e.target.closest('[data-navigation-document-option]'); if (item) { self.applyPickedDocument({ id: item.getAttribute('data-id'), title: item.getAttribute('data-title'), alias: item.getAttribute('data-alias') }); close(); } }); search.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(load, 220); }); document.addEventListener('keydown', onKey); load(); search.focus(); }, applyPickedDocument: function (item) { var id = item ? parseInt(item.id || item.document_id, 10) || 0 : 0; var title = item ? String(item.title || item.document_title || '') : ''; var alias = item ? String(item.alias || item.document_alias || '').replace(/^\/+/, '') : ''; var input = this.itemField('document_id'); if (input) { input.value = id || ''; } this.renderPickedDocument(id ? { document_id: id, document_title: title, document_alias: alias } : null); if (!id) { return; } var titleInput = this.itemField('title'); var aliasInput = this.itemField('alias'); if (titleInput) { titleInput.value = title; titleInput.dispatchEvent(new Event('input', { bubbles: true })); } if (aliasInput) { aliasInput.value = alias; aliasInput.dispatchEvent(new Event('input', { bubbles: true })); } }, renderPickedDocument: function (item) { var id = item ? parseInt(item.document_id || item.id, 10) || 0 : 0; var title = item ? String(item.document_title || item.title || '') : ''; var display = document.querySelector('[data-navigation-document-display]'); var clear = document.querySelector('[data-navigation-document-clear]'); if (display) { display.value = id ? ('#' + id + (title ? ' · ' + title : '')) : ''; } if (clear) { clear.hidden = !id; } }, clearPickedDocument: function () { var input = this.itemField('document_id'); if (input) { input.value = ''; } this.renderPickedDocument(null); }, openImagePicker: function () { var self = this; Adminx.MediaPicker.open({ type: 'image', title: 'Выбрать изображение', description: 'Файл из медиабраузера будет привязан к пункту меню.', onPick: function (file) { self.applyPickedImage((file && file.url) || ''); } }); }, applyPickedImage: function (url) { var input = this.itemField('image'); url = String(url || ''); if (input) { input.value = url; input.dispatchEvent(new Event('input', { bubbles: true })); } this.renderPickedImage(url); }, renderPickedImage: function (url) { var selection = document.querySelector('[data-navigation-image-selection]'); var image = selection ? selection.querySelector('img') : null; var name = document.querySelector('[data-navigation-image-name]'); var clear = document.querySelector('[data-navigation-image-clear]'); url = String(url || ''); if (selection) { selection.hidden = !url; } if (image) { image.src = url; } if (name) { name.textContent = url ? url.split('/').pop() : ''; } if (clear) { clear.hidden = !url; } }, submitItem: function () { var id = this.itemField('navigation_item_id').value; var navId = this.itemField('navigation_id').value || this.currentNavigationId; var url = id ? this.base() + '/navigation/items/' + encodeURIComponent(id) : this.base() + '/navigation/' + encodeURIComponent(navId) + '/items'; var self = this; this.clearItemErrors(); this.post(url, new FormData(this.itemForm)).then(function (payload) { Adminx.Toast.show(payload.message || 'Пункт сохранён', 'success'); if (payload.data && payload.data.id) { self.currentItemId = parseInt(payload.data.id, 10) || self.currentItemId; self.itemField('navigation_item_id').value = payload.data.id; } self.loadItems(); self.applyFilterUrl(window.location.href, false); }).catch(function (err) { self.showItemErrors(err); }); }, toggleItem: function (id) { var data = new FormData(); data.append('_csrf', this.csrf()); var self = this; this.post(this.base() + '/navigation/items/' + encodeURIComponent(id) + '/toggle', data) .then(function (payload) { Adminx.Toast.show(payload.message || 'Статус изменён', 'success'); self.loadItems(); }) .catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось изменить статус', 'error'); }); }, deleteItem: function (id) { var data = new FormData(); data.append('_csrf', this.csrf()); var self = this; var run = function () { self.post(self.base() + '/navigation/items/' + encodeURIComponent(id) + '/delete', data) .then(function (payload) { Adminx.Toast.show(payload.message || 'Пункт удалён', 'success'); if (String(self.currentItemId) === String(id)) { self.currentItemId = 0; self.setItemFormMode('empty'); } self.loadItems(); self.applyFilterUrl(window.location.href, false); }) .catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось удалить пункт', 'error'); }); }; if (Adminx.Confirm) { Adminx.Confirm.open({ kind: 'danger', title: 'Удалить пункт меню', message: 'Если у пункта есть дочерние элементы, он будет выключен.', confirmLabel: 'Удалить', onConfirm: run }); } else { run(); } }, itemDragStart: function (e) { var handle = e.target.closest('[data-navigation-drag-handle]'); var root = handle ? handle.closest('[data-navigation-items-tree]') : null; if (!handle || !root || this.orderSaving || (typeof e.button === 'number' && e.button !== 0)) { return; } this.dragItem = handle.closest('[data-navigation-item-node]'); if (!this.dragItem) { return; } this.dragPointerId = e.pointerId; this.dragStartX = e.clientX; this.dragStartY = e.clientY; this.dragStarted = false; this.dragOrderSnapshot = this.itemOrderSignature(); if (handle.setPointerCapture) { handle.setPointerCapture(e.pointerId); } e.preventDefault(); }, beginItemDrag: function (e) { if (!this.dragItem || this.dragStarted) { return; } var row = this.dragItem.querySelector('[data-navigation-builder-item]'); var firstRect = this.dragItem.getBoundingClientRect(); var rowRect = row.getBoundingClientRect(); this.dragStarted = true; this.dragGroup = this.itemBranch(this.dragItem); this.dragPlaceholder = document.createElement('li'); this.dragPlaceholder.className = 'navigation-drag-placeholder'; this.dragPlaceholder.setAttribute('aria-hidden', 'true'); var lastRect = this.dragGroup[this.dragGroup.length - 1].getBoundingClientRect(); this.dragPlaceholder.style.height = Math.max(48, lastRect.bottom - firstRect.top) + 'px'; this.dragItem.parentNode.insertBefore(this.dragPlaceholder, this.dragItem); this.dragGhost = row.cloneNode(true); this.dragGhost.classList.remove('is-selected', 'mb-level-0', 'mb-level-1', 'mb-level-2'); this.dragGhost.classList.add('navigation-drag-ghost'); this.dragGhost.style.width = Math.min(rowRect.width, 440, Math.max(1, window.innerWidth - 24)) + 'px'; document.body.appendChild(this.dragGhost); this.positionItemDragGhost(e); document.body.classList.add('navigation-item-dragging'); this.dragItem.closest('[data-navigation-items-tree]').classList.add('is-drag-active'); this.dragGroup.forEach(function (node, index) { node.classList.add(index === 0 ? 'is-dragging' : 'is-dragging-child'); }); }, itemDragOver: 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.beginItemDrag(e); } e.preventDefault(); this.positionItemDragGhost(e); var root = this.dragItem.closest('[data-navigation-items-tree]'); if (!root) { return; } this.scrollItemTree(root, e.clientY); var target = null; var bottom = false; var self = this; Array.prototype.some.call(root.querySelectorAll(':scope > ol > [data-navigation-item-node]'), function (node) { if (self.dragGroup.indexOf(node) !== -1) { return false; } var candidate = node.querySelector('[data-navigation-builder-item]'); var candidateRect = candidate ? candidate.getBoundingClientRect() : null; if (!candidateRect || e.clientY >= candidateRect.bottom) { return false; } target = node; bottom = e.clientY > candidateRect.top + candidateRect.height / 2; return true; }); document.querySelectorAll('.navigation-tree-item.drag-over-top, .navigation-tree-item.drag-over-bottom').forEach(function (node) { node.classList.remove('drag-over-top', 'drag-over-bottom'); }); if (!target) { root.querySelector('ol').appendChild(this.dragPlaceholder); return; } var row = target.querySelector('[data-navigation-builder-item]'); var anchor = bottom ? this.itemBranch(target).slice(-1)[0].nextSibling : target; if (anchor !== this.dragPlaceholder) { target.parentNode.insertBefore(this.dragPlaceholder, anchor); } row.classList.toggle('drag-over-top', !bottom); row.classList.toggle('drag-over-bottom', bottom); }, itemDrop: function (e) { if (!this.dragItem || e.pointerId !== this.dragPointerId) { return; } e.preventDefault(); if (!this.dragStarted || !this.dragPlaceholder || !this.dragPlaceholder.parentNode) { this.itemDragEnd(); return; } var placeholder = this.dragPlaceholder; this.dragGroup.forEach(function (node) { placeholder.parentNode.insertBefore(node, placeholder); }); placeholder.parentNode.removeChild(placeholder); this.dragPlaceholder = null; this.normalizeItemHierarchy(); var changed = this.itemOrderSignature() !== this.dragOrderSnapshot; this.itemDragEnd(); if (changed) { this.persistItemOrder(); } }, itemDragEnd: 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('.navigation-tree-item.drag-over-top, .navigation-tree-item.drag-over-bottom').forEach(function (node) { node.classList.remove('drag-over-top', 'drag-over-bottom'); }); document.querySelectorAll('[data-navigation-items-tree].is-drag-active').forEach(function (node) { node.classList.remove('is-drag-active'); }); if (this.dragGhost && this.dragGhost.parentNode) { this.dragGhost.parentNode.removeChild(this.dragGhost); } document.body.classList.remove('navigation-item-dragging'); this.dragItem = null; this.dragGroup = []; this.dragPlaceholder = null; this.dragGhost = null; this.dragPointerId = null; this.dragStarted = false; this.dragOrderSnapshot = ''; }, positionItemDragGhost: 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)'; }, scrollItemTree: function (root, clientY) { var rect = root.getBoundingClientRect(); var edge = 54; if (clientY < rect.top + edge) { root.scrollTop -= Math.ceil((rect.top + edge - clientY) / 5); } if (clientY > rect.bottom - edge) { root.scrollTop += Math.ceil((clientY - rect.bottom + edge) / 5); } }, itemOrderSignature: function () { return Array.prototype.map.call(document.querySelectorAll('[data-navigation-items-tree] > ol > [data-navigation-item-node]'), function (node) { return node.getAttribute('data-navigation-item-node') + ':' + node.getAttribute('data-level'); }).join('|'); }, itemBranch: function (node) { if (!node) { return []; } var branch = [node]; var level = parseInt(node.getAttribute('data-level'), 10) || 0; var next = node.nextElementSibling; while (next) { if (next === this.dragPlaceholder) { next = next.nextElementSibling; continue; } if (parseInt(next.getAttribute('data-level'), 10) <= level) { break; } branch.push(next); next = next.nextElementSibling; } return branch; }, changeItemLevel: function (node, delta) { if (!node || !delta) { return; } var level = parseInt(node.getAttribute('data-level'), 10) || 0; var nextLevel = Math.max(0, Math.min(2, level + delta)); if (delta > 0 && !this.canIndentItem(node)) { return; } if (nextLevel === level) { return; } node.setAttribute('data-level', String(nextLevel)); this.normalizeItemHierarchy(); this.persistItemOrder(); }, canIndentItem: function (node) { var level = parseInt(node.getAttribute('data-level'), 10) || 0; if (level >= 2) { return false; } var previous = node.previousElementSibling; while (previous) { var previousLevel = parseInt(previous.getAttribute('data-level'), 10) || 0; if (previousLevel < level) { return false; } if (previousLevel === level) { return true; } previous = previous.previousElementSibling; } return false; }, normalizeItemHierarchy: function () { var nodes = document.querySelectorAll('[data-navigation-items-tree] > ol > [data-navigation-item-node]'); var parents = []; var positions = {}; var previousLevel = 0; var self = this; nodes.forEach(function (node, index) { var level = Math.max(0, Math.min(2, 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 = level > 0 && parents[level - 1] ? parents[level - 1] : 0; var id = parseInt(node.getAttribute('data-navigation-item-node'), 10) || 0; var positionKey = String(parentId); positions[positionKey] = (positions[positionKey] || 0) + 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(positions[positionKey])); var row = node.querySelector('[data-navigation-builder-item]'); if (row) { row.classList.remove('mb-level-0', 'mb-level-1', 'mb-level-2'); row.classList.add('mb-level-' + level); } var pos = node.querySelector('.si-pos'); if (pos) { pos.textContent = String(positions[positionKey]); } var badge = node.querySelector('[data-navigation-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-navigation-item-indent="-1"]'); var increase = node.querySelector('[data-navigation-item-indent="1"]'); if (decrease) { decrease.disabled = level === 0; } if (increase) { increase.disabled = !self.canIndentItem(node); } }); }, persistItemOrder: function () { var order = []; document.querySelectorAll('[data-navigation-items-tree] > ol > [data-navigation-item-node]').forEach(function (node) { var parentId = parseInt(node.getAttribute('data-parent-id'), 10) || 0; order.push({ id: parseInt(node.getAttribute('data-navigation-item-node'), 10) || 0, parent_id: parentId, position: parseInt(node.getAttribute('data-position'), 10) || 1 }); }); var data = new FormData(); data.append('_csrf', this.csrf()); data.append('order', JSON.stringify(order)); var self = this; var root = document.querySelector('[data-navigation-items-tree]'); this.orderSaving = true; if (root) { root.classList.add('is-order-saving'); } this.post(this.base() + '/navigation/' + encodeURIComponent(this.currentNavigationId) + '/items/reorder', data) .then(function (payload) { Adminx.Toast.show(payload.message || 'Порядок сохранён', 'success'); self.loadItems(); }) .catch(function (err) { Adminx.Toast.show(err.message || 'Не удалось сохранить порядок', 'error'); self.loadItems(); }).finally(function () { self.orderSaving = false; var currentRoot = document.querySelector('[data-navigation-items-tree]'); if (currentRoot) { currentRoot.classList.remove('is-order-saving'); } }); }, ajaxRefresh: function (message, keepOpen) { if (message) { Adminx.Toast.show(message, 'success'); } if (!keepOpen && Adminx.Drawer) { Adminx.Drawer.close(); } this.applyFilterUrl(window.location.href, false); if (keepOpen && window.Adminx.CodeEditor) { setTimeout(function () { Adminx.CodeEditor.refreshAll(); }, 120); } }, clearErrors: function () { if (!this.form) { return; } this.form.querySelectorAll('[data-error]').forEach(function (el) { el.textContent = ''; }); this.form.querySelectorAll('.is-invalid').forEach(function (el) { el.classList.remove('is-invalid'); }); }, clearItemErrors: function () { if (!this.itemForm) { return; } this.itemForm.querySelectorAll('[data-error]').forEach(function (el) { el.textContent = ''; }); this.itemForm.querySelectorAll('.is-invalid').forEach(function (el) { el.classList.remove('is-invalid'); }); }, showErrors: function (err) { if (err && err.errors) { Object.keys(err.errors).forEach(function (key) { var node = document.querySelector('[data-error="' + key + '"]'); var field = document.querySelector('[name="' + key + '"]'); if (node) { node.textContent = err.errors[key]; } if (field) { field.classList.add('is-invalid'); } }); } Adminx.Toast.show((err && err.message) || 'Проверьте поля формы', 'error'); }, showItemErrors: function (err) { if (err && err.errors) { Object.keys(err.errors).forEach(function (key) { var node = document.querySelector('[data-error="' + key + '"]'); if (node) { node.textContent = err.errors[key]; } }); } Adminx.Toast.show((err && err.message) || 'Проверьте поля пункта', 'error'); }, esc: function (value) { return String(value == null ? '' : value).replace(/[&<>"']/g, function (ch) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]; }); }, csrf: function () { var token = this.form ? this.form.querySelector('[name="_csrf"]') : document.querySelector('[name="_csrf"]'); return token ? token.value : ''; }, post: function (url, data) { return fetch(url, { method: 'POST', body: data, headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json); }, json: function (res) { return res.json().then(function (payload) { if (!res.ok || payload.success === false) { var err = new Error(payload.message || ('HTTP ' + res.status)); err.errors = payload.errors || {}; throw err; } return payload; }); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { Adminx.Navigation.init(); }); } else { Adminx.Navigation.init(); } })(window, document);