/** * JS раздела «Рубрики и поля»: AJAX-фильтры, drawer-CRUD и сортировка. */ (function (window, document) { 'use strict'; var Adminx = window.Adminx || (window.Adminx = {}); function esc(value) { return String(value == null ? '' : value).replace(/[&<>"']/g, function (ch) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]; }); } Adminx.Rubrics = { form: null, fieldForm: null, groupForm: null, templateForm: null, extraTemplateForm: null, builderForm: null, currentRubricId: 0, currentGroups: [], currentFields: [], currentFieldSetLinks: [], fieldSetMode: 'copy', currentTemplateFields: [], activeTemplateTextarea: null, filterTimer: null, aliasTimer: null, fieldAliasTimer: null, dragRubricRow: null, dragGroupNode: null, dragFieldNode: null, dragBuilderFieldId: 0, builderSelectedId: 0, builderDrafts: {}, builderDirty: false, builderHydrating: false, builderSettingsReady: false, adminViewForm: null, adminViewState: null, adminViewFields: [], adminViewPreview: [], adminViewSelected: '', adminViewDragSource: '', templateRegistryLoaded: false, templateRegistryLoading: false, currentSchemaRevisionRubricId: 0, currentSchemaRevisionId: 0, currentSchemaRevisionFingerprint: '', init: function () { this.form = document.getElementById('rubricForm'); this.fieldForm = document.getElementById('rubricFieldForm'); this.groupForm = document.getElementById('rubricGroupForm'); this.templateForm = document.getElementById('rubricTemplateForm'); this.extraTemplateForm = document.getElementById('rubricExtraTemplateForm'); this.builderForm = document.querySelector('[data-builder-inspector]'); this.adminViewForm = document.getElementById('rubricAdminViewForm'); this.syncFieldTypeOptions(''); this.bind(); }, base: function () { return (this.form && this.form.getAttribute('data-base')) || Adminx.base(); }, bind: function () { var self = this; document.addEventListener('click', function (e) { var cmWrap = e.target.closest('.CodeMirror'); if (cmWrap) { self.captureCodeMirror(cmWrap); } var mainTab = e.target.closest('[data-rubrics-main-tab]'); if (mainTab) { self.activateMainTab(mainTab.getAttribute('data-rubrics-main-tab')); } if (e.target.closest('[data-rubrics-filter-reset]')) { self.resetFilters(); } if (e.target.closest('[data-rubric-new]')) { self.fillRubricNew(); } var edit = e.target.closest('[data-rubric-edit]'); if (edit) { self.fillRubricEdit(edit.closest('[data-rubric-row]')); } var del = e.target.closest('[data-rubric-delete]'); if (del) { self.deleteRubric(del.closest('[data-rubric-row]')); } var fields = e.target.closest('[data-rubric-fields]'); if (fields) { self.openFields(fields.closest('[data-rubric-row]')); } var templates = e.target.closest('[data-rubric-templates]'); if (templates) { self.openTemplates(templates.closest('[data-rubric-row]')); } var adminView = e.target.closest('[data-rubric-admin-view]'); if (adminView) { self.openAdminView(adminView.closest('[data-rubric-row]')); } var schemaRevisions = e.target.closest('[data-rubric-schema-revisions]'); if (schemaRevisions) { self.openSchemaRevisions(schemaRevisions.closest('[data-rubric-row]')); } var schemaRevisionOpen = e.target.closest('[data-schema-revision-open]'); if (schemaRevisionOpen && !e.target.closest('[data-schema-revision-row-delete]')) { self.loadSchemaRevision(schemaRevisionOpen.getAttribute('data-schema-revision-open')); } var schemaRevisionRowDelete = e.target.closest('[data-schema-revision-row-delete]'); if (schemaRevisionRowDelete) { self.deleteSchemaRevision(schemaRevisionRowDelete.getAttribute('data-schema-revision-row-delete')); } if (e.target.closest('[data-schema-revision-delete]')) { self.deleteSchemaRevision(); } if (e.target.closest('[data-schema-revisions-clear]')) { self.clearSchemaRevisions(); } if (e.target.closest('[data-schema-revision-restore]')) { self.restoreSchemaRevision(); } var adminViewMode = e.target.closest('[data-admin-view-mode]'); if (adminViewMode) { self.setAdminViewMode(adminViewMode.getAttribute('data-admin-view-mode')); } var adminViewAdd = e.target.closest('[data-admin-view-add]'); if (adminViewAdd) { self.addAdminViewItem(adminViewAdd.getAttribute('data-admin-view-add')); } var adminViewItem = e.target.closest('[data-admin-view-item]'); if (adminViewItem && !e.target.closest('[data-admin-view-remove], [data-admin-view-move]')) { self.selectAdminViewItem(adminViewItem.getAttribute('data-source')); } var adminViewRemove = e.target.closest('[data-admin-view-remove]'); if (adminViewRemove) { self.removeAdminViewItem(adminViewRemove.getAttribute('data-admin-view-remove')); } var adminViewMove = e.target.closest('[data-admin-view-move]'); if (adminViewMove) { self.moveAdminViewItem(adminViewMove.getAttribute('data-source'), parseInt(adminViewMove.getAttribute('data-admin-view-move'), 10) || 0); } var adminViewToken = e.target.closest('[data-admin-view-token]'); if (adminViewToken) { self.insertAdminViewToken(adminViewToken.getAttribute('data-admin-view-token')); } if (e.target.closest('[data-admin-view-template-default]')) { self.setAdminViewTemplateExample(); } var templateTab = e.target.closest('[data-rubric-template-tab]'); if (templateTab) { self.activateTemplateTab(templateTab.getAttribute('data-rubric-template-tab')); } if (e.target.closest('[data-rubric-og-default]')) { self.setOpenGraphExample(); } if (e.target.closest('[data-rubric-template-refresh]')) { self.loadTemplates(); } var templateTagsToggle = e.target.closest('[data-rubric-template-tags-toggle]'); if (templateTagsToggle) { self.openTemplateTagPalette(templateTagsToggle.closest('.rubrics-code-field')); } if (e.target.closest('[data-rubric-template-tags-close]')) { self.closeTemplateTagPalette(); } var templateTagTab = e.target.closest('[data-rubric-template-tag-tab]'); if (templateTagTab) { self.activateTemplateTagGroup(templateTagTab.getAttribute('data-rubric-template-tag-tab')); } var templateTag = e.target.closest('[data-rubric-template-tag]'); if (templateTag) { self.insertTemplateTag(templateTag); } var fieldTplTag = e.target.closest('[data-field-tpl-tag]'); if (fieldTplTag) { e.preventDefault(); self.insertFieldTemplateTag(fieldTplTag.getAttribute('data-field-tpl-tag')); } var codeLint = e.target.closest('[data-rubric-code-lint]'); if (codeLint) { self.lintCodeField(codeLint.closest('.rubrics-code-field')); } var codeFull = e.target.closest('[data-rubric-code-fullscreen]'); if (codeFull) { self.toggleCodeFieldFullscreen(codeFull.closest('.rubrics-code-field')); } if (e.target.closest('[data-extra-template-new]')) { self.fillExtraTemplateNew(); } var extraEdit = e.target.closest('[data-extra-template-edit]'); if (extraEdit) { self.fillExtraTemplateEdit(extraEdit.closest('[data-extra-template-row]')); } var extraDelete = e.target.closest('[data-extra-template-delete]'); if (extraDelete) { self.deleteExtraTemplate(extraDelete.closest('[data-extra-template-row]')); } if (e.target.closest('[data-field-new]')) { self.fillFieldNew(); } var builderReturn = e.target.closest('[data-builder-unplace]'); if (builderReturn) { e.preventDefault(); self.unplaceBuilderField(builderReturn.closest('[data-field-row]')); } var builderSelect = e.target.closest('[data-builder-select]'); if (builderSelect && !e.target.closest('[data-field-edit], [data-field-delete], [data-field-tag-copy], [data-builder-unplace]')) { if (builderSelect.matches('[data-builder-palette-item]')) { self.placeBuilderField(builderSelect.getAttribute('data-id'), 0); } else { self.selectBuilderField(builderSelect.getAttribute('data-id')); } } if (e.target.closest('[data-builder-save]')) { self.saveBuilder(); } if (e.target.closest('[data-builder-advanced]')) { self.openBuilderAdvanced(); } if (e.target.closest('[data-builder-field-set-apply]')) { self.applyBuilderFieldSet(); } if (e.target.closest('[data-builder-field-set-export]')) { self.exportBuilderFieldSet(); } if (e.target.closest('[data-builder-field-set-import]')) { self.openBuilderFieldSetImport(); } var fieldSetMode = e.target.closest('[data-field-set-mode]'); if (fieldSetMode) { self.setBuilderFieldSetMode(fieldSetMode.getAttribute('data-field-set-mode')); } var fieldSetSync = e.target.closest('[data-field-set-sync]'); if (fieldSetSync) { self.previewLinkedFieldSet(fieldSetSync.getAttribute('data-field-set-sync')); } var fieldSetDetach = e.target.closest('[data-field-set-detach]'); if (fieldSetDetach) { self.detachLinkedFieldSet(fieldSetDetach.getAttribute('data-field-set-detach')); } var fieldEdit = e.target.closest('[data-field-edit]'); if (fieldEdit) { if (self.builderDirty && fieldEdit.closest('[data-builder-canvas]')) { Adminx.Toast.show('Сначала сохраните изменения конструктора', 'warning'); return; } self.fillFieldEdit(fieldEdit.closest('[data-field-row]')); if (Adminx.Drawer) { Adminx.Drawer.open('rubricFieldDrawer'); } } var fieldDelete = e.target.closest('[data-field-delete]'); if (fieldDelete) { self.deleteField(fieldDelete.closest('[data-field-row]')); } var fieldCopy = e.target.closest('[data-field-tag-copy]'); if (fieldCopy) { self.copy(fieldCopy.getAttribute('data-field-tag-copy')); } var defaultBool = e.target.closest('[data-default-boolean]'); if (defaultBool) { e.preventDefault(); self.setDefaultBoolean(defaultBool.getAttribute('data-default-boolean')); } if (e.target.closest('[data-default-list-add]')) { e.preventDefault(); self.addDefaultListRow(); } var defaultListRemove = e.target.closest('[data-default-list-remove]'); if (defaultListRemove) { e.preventDefault(); self.removeDefaultListRow(defaultListRemove.closest('[data-default-list-row]')); } var settingsMapAdd = e.target.closest('[data-settings-map-add]'); if (settingsMapAdd) { e.preventDefault(); self.addSettingsMapRow(settingsMapAdd.closest('[data-settings-map]')); } var settingsMapRemove = e.target.closest('[data-settings-map-remove]'); if (settingsMapRemove) { e.preventDefault(); self.removeSettingsMapRow(settingsMapRemove.closest('[data-settings-map-row]')); } if (e.target.closest('[data-builder-condition-add]')) { e.preventDefault(); self.addBuilderConditionRow(e.target.closest('[data-builder-condition-group]')); } if (e.target.closest('[data-builder-condition-group-add]')) { e.preventDefault(); self.addBuilderConditionGroup(e.target.closest('[data-builder-condition-group]')); } var conditionRemove = e.target.closest('[data-builder-condition-remove]'); if (conditionRemove) { e.preventDefault(); self.removeBuilderConditionRow(conditionRemove.closest('[data-builder-condition-row]')); } var conditionGroupRemove = e.target.closest('[data-builder-condition-group-remove]'); if (conditionGroupRemove) { e.preventDefault(); self.removeBuilderConditionGroup(conditionGroupRemove.closest('[data-builder-condition-group]')); } if (e.target.closest('[data-default-media-pick]')) { e.preventDefault(); self.openDefaultMediaPicker(null); } var mediaRowPick = e.target.closest('[data-default-media-row-pick]'); if (mediaRowPick) { e.preventDefault(); self.openDefaultMediaPicker(mediaRowPick.closest('[data-default-list-row]')); } if (e.target.closest('[data-default-relation-pick]')) { e.preventDefault(); self.openDefaultRelationPicker(null); } var relationRowPick = e.target.closest('[data-default-relation-row-pick]'); if (relationRowPick) { e.preventDefault(); self.openDefaultRelationPicker(relationRowPick.closest('[data-default-list-row]')); } if (e.target.closest('[data-group-new]')) { self.fillGroupNew(); } var groupEdit = e.target.closest('[data-group-edit]'); if (groupEdit) { self.fillGroupEdit(groupEdit.closest('[data-group-row]')); if (Adminx.Drawer) { Adminx.Drawer.open('rubricGroupDrawer'); } } var groupDelete = e.target.closest('[data-group-delete]'); if (groupDelete) { self.deleteGroup(groupDelete.closest('[data-group-row]')); } }); document.addEventListener('submit', function (e) { var filter = e.target.closest('.rubrics-filter'); if (filter) { e.preventDefault(); self.applyFilters(filter, true); } }); document.addEventListener('change', function (e) { if (e.target.matches('[data-field-type-toggle]')) { self.toggleFieldType(e.target); return; } if (e.target.matches('[data-field-types-state]')) { self.filterFieldTypes(); } if (e.target.matches('[data-rubric-preset]')) { self.syncRubricPresetHint(); } if (e.target.matches('[data-builder-field-set]')) { self.syncBuilderFieldSet(); } var filter = e.target.closest('.rubrics-filter'); if (filter && e.target.matches('select')) { self.applyFilters(filter, true); } if (self.fieldForm && e.target === self.fieldForm.elements.rubric_field_type) { self.syncFieldTypeOptions(e.target.value); self.syncNativeNumericType(e.target.value); self.loadTypeInfo(e.target.value); } if (e.target.matches('[data-default-editor-input]')) { self.syncDefaultEditorValue(e.target); } if (e.target.matches('[data-default-list-input], [data-default-list-key], [data-default-list-value]')) { self.syncDefaultListValue(); } if (e.target.matches('[data-settings-map-key], [data-settings-map-label]')) { self.syncSettingsMap(e.target.closest('[data-settings-map]')); } if (e.target.matches('[data-builder-conditions-enabled]')) { self.syncBuilderRuntime(); self.setBuilderDirty(true); } var changedCondition = e.target.closest('[data-builder-condition]'); if (changedCondition && e.target.matches('[data-builder-condition-enabled], [data-builder-condition-mode], [data-builder-condition-operator], [data-builder-condition-join], [data-builder-condition-options-enabled], [data-builder-condition-value-action], [data-builder-condition-action-select]')) { self.syncBuilderCondition(changedCondition); } if (self.builderForm && self.builderForm.contains(e.target)) { self.captureBuilderInspector(); if (e.target.matches('[data-builder-group]')) { self.moveSelectedBuilderField(e.target.value); } if (e.target.matches('input[name="width"]')) { self.applyBuilderWidth(e.target.value); } } if (e.target.matches('[data-admin-view-format], input[name="admin_view_width"]')) { self.captureAdminViewInspector(); } }); document.addEventListener('input', function (e) { var filter = e.target.closest('.rubrics-filter'); if (filter && e.target.matches('input[type="search"]')) { clearTimeout(self.filterTimer); self.filterTimer = setTimeout(function () { self.applyFilters(filter, true); }, 350); } if (self.form && e.target === self.form.elements.rubric_alias) { self.updateRubricAliasPreview(); clearTimeout(self.aliasTimer); self.aliasTimer = setTimeout(function () { self.checkRubricAlias(false); }, 250); } if (self.fieldForm && e.target === self.fieldForm.elements.rubric_field_alias) { clearTimeout(self.fieldAliasTimer); self.fieldAliasTimer = setTimeout(function () { self.checkFieldAlias(false); }, 250); } if (e.target.matches('[data-default-editor-input]')) { self.syncDefaultEditorValue(e.target); } if (e.target.matches('[data-default-list-input], [data-default-list-key], [data-default-list-value]')) { self.syncDefaultListValue(); } if (e.target.matches('[data-settings-map-key], [data-settings-map-label]')) { self.syncSettingsMap(e.target.closest('[data-settings-map]')); } if (e.target.matches('[data-builder-search]')) { self.filterBuilderPalette(e.target.value); } if (e.target.matches('[data-rubric-template-tags-search]')) { self.filterTemplateTags(e.target.value); } if (e.target.matches('[data-field-types-search]')) { self.filterFieldTypes(); } if (self.builderForm && self.builderForm.contains(e.target)) { self.captureBuilderInspector(); if (e.target.matches('[name="rubric_field_settings[options]"]')) { self.syncBuilderConditionOptionSource(e.target.value); } } if (e.target.matches('[data-admin-view-search]')) { self.renderAdminViewFields(e.target.value); } if (e.target.matches('[data-admin-view-label]')) { self.captureAdminViewInspector(); } if (self.adminViewForm && e.target === self.adminViewForm.elements.template) { self.renderAdminViewPreview(); } }); document.addEventListener('focusin', function (e) { if (e.target && e.target.matches && e.target.matches('#rubricTemplateForm textarea, #rubricExtraTemplateForm textarea')) { self.activeTemplateTextarea = e.target; } }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { if (document.querySelector('.rubrics-code-field.is-fullscreen')) { self.closeCodeFullscreen(); } else { self.closeTemplateTagPalette(); } } }); window.addEventListener('popstate', function () { self.applyFilterUrl(window.location.href, false); }); document.addEventListener('adminx:drawer:closed', function (e) { var drawer = e.detail && e.detail.drawer ? e.detail.drawer : null; if (drawer && drawer.id === 'rubricFieldDrawer') { self.resetFieldForm(); } if (drawer && (drawer.id === 'rubricTemplatesDrawer' || drawer.id === 'rubricExtraTemplateDrawer')) { self.closeTemplateTagPalette(); } }); document.addEventListener('dragstart', function (e) { if (e.target.closest('[data-admin-view-item]')) { self.adminViewDragStart(e); return; } self.dragStart(e); }); document.addEventListener('dragover', function (e) { if (self.adminViewDragSource) { self.adminViewDragOver(e); return; } self.dragOver(e); }); document.addEventListener('drop', function (e) { if (self.adminViewDragSource) { self.adminViewDrop(e); return; } self.drop(e); }); document.addEventListener('dragend', function () { self.adminViewDragEnd(); self.dragEnd(); }); if (this.form) { this.form.addEventListener('submit', function (e) { e.preventDefault(); self.submitRubric(); }); } if (this.fieldForm) { this.fieldForm.addEventListener('submit', function (e) { e.preventDefault(); self.submitField(); }); } if (this.groupForm) { this.groupForm.addEventListener('submit', function (e) { e.preventDefault(); self.submitGroup(); }); } if (this.templateForm) { this.templateForm.addEventListener('submit', function (e) { e.preventDefault(); self.submitMainTemplate(); }); } if (this.extraTemplateForm) { this.extraTemplateForm.addEventListener('submit', function (e) { e.preventDefault(); self.submitExtraTemplate(); }); } if (this.builderForm) { this.builderForm.addEventListener('submit', function (e) { e.preventDefault(); self.saveBuilder(); }); } if (this.adminViewForm) { this.adminViewForm.addEventListener('submit', function (e) { e.preventDefault(); self.saveAdminView(); }); } }, filterUrl: function (form) { var params = new URLSearchParams(new FormData(form)); Array.from(params.keys()).forEach(function (key) { var value = String(params.get(key) || ''); if (value === '') { params.delete(key); } }); var query = params.toString(); return (form.getAttribute('action') || (this.base() + '/rubrics')) + (query ? '?' + query : ''); }, applyFilters: function (form, push) { 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'); ['.rubrics-summary', '.rubrics-import-card', '.rubrics-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({ adminxRubricsFilters: true }, '', url); } }, activateMainTab: function (name) { name = name || 'rubrics'; document.querySelectorAll('[data-rubrics-main-tab]').forEach(function (tab) { tab.setAttribute('aria-selected', tab.getAttribute('data-rubrics-main-tab') === name ? 'true' : 'false'); }); document.querySelectorAll('[data-rubrics-main-panel]').forEach(function (panel) { panel.hidden = panel.getAttribute('data-rubrics-main-panel') !== name; }); }, resetFilters: function () { var form = document.querySelector('.rubrics-filter'); if (!form) { return; } form.reset(); Array.prototype.forEach.call(form.querySelectorAll('input, select'), function (el) { if (el.tagName === 'SELECT') { el.selectedIndex = 0; } else { el.value = ''; } }); this.applyFilters(form, true); }, toggleFieldType: function (input) { if (!input || !this.form) { return; } var self = this; var type = input.getAttribute('data-field-type-toggle') || ''; var enabled = input.checked; var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); data.append('enabled', enabled ? '1' : '0'); input.disabled = true; this.ajax(this.base() + '/rubrics/field-types/' + encodeURIComponent(type) + '/toggle', data, function (json) { var payload = json.data || {}; var result = payload.type || {}; var actual = !!result.enabled; var row = input.closest('[data-field-type-row]'); input.checked = actual; input.disabled = false; input.setAttribute('aria-label', (actual ? 'Отключить' : 'Включить') + ' тип ' + (result.name || type)); var switchRoot = input.closest('.rubrics-type-switch'); if (switchRoot) { switchRoot.setAttribute('data-tooltip', actual ? 'Убрать из выбора новых полей' : 'Добавить в выбор новых полей'); } if (row) { row.setAttribute('data-state', actual ? 'enabled' : 'disabled'); row.classList.toggle('is-disabled', !actual); } var option = null; if (self.fieldForm) { Array.prototype.some.call(self.fieldForm.querySelectorAll('[data-field-type-option]'), function (candidate) { if (candidate.value !== type) { return false; } option = candidate; return true; }); } if (option) { var label = option.getAttribute('data-field-type-label') || result.name || type; option.setAttribute('data-enabled', actual ? '1' : '0'); option.textContent = label + (actual ? '' : ' (отключён)'); } var summary = payload.summary || {}; document.querySelectorAll('[data-field-types-enabled]').forEach(function (node) { if (typeof summary.enabled !== 'undefined') { node.textContent = summary.enabled; } }); document.querySelectorAll('[data-field-types-disabled]').forEach(function (node) { if (typeof summary.disabled !== 'undefined') { node.textContent = summary.disabled; } }); var currentType = self.fieldForm && self.fieldForm.elements.id.value ? self.fieldForm.elements.rubric_field_type.value : ''; self.syncFieldTypeOptions(currentType); self.filterFieldTypes(); Adminx.Toast.show(json.message || (actual ? 'Тип поля включён' : 'Тип поля отключён'), 'success'); }, function (json) { input.checked = !enabled; input.disabled = false; Adminx.Toast.show(json.message || 'Не удалось изменить состояние типа поля', 'error'); }); }, filterFieldTypes: function () { var search = document.querySelector('[data-field-types-search]'); var state = document.querySelector('[data-field-types-state]'); var needle = search ? String(search.value || '').trim().toLowerCase() : ''; var selected = state ? String(state.value || '') : ''; var visible = 0; document.querySelectorAll('[data-field-type-row]').forEach(function (row) { var matchesSearch = !needle || String(row.getAttribute('data-search') || '').indexOf(needle) !== -1; var rowState = row.getAttribute('data-state') || ''; var isNew = row.getAttribute('data-new') === '1'; var usage = parseInt(row.getAttribute('data-usage') || '0', 10); var matchesState = !selected || (selected === 'used' ? usage > 0 : (selected === 'new' ? isNew : rowState === selected)); row.hidden = !(matchesSearch && matchesState); if (!row.hidden) { visible += 1; } }); var empty = document.querySelector('[data-field-types-empty]'); if (empty) { empty.hidden = visible > 0; } }, syncFieldTypeOptions: function (currentType) { if (!this.fieldForm || !this.fieldForm.elements.rubric_field_type) { return; } var select = this.fieldForm.elements.rubric_field_type; currentType = String(currentType || ''); Array.prototype.forEach.call(select.querySelectorAll('[data-field-type-option]'), function (option) { option.disabled = option.getAttribute('data-enabled') !== '1' && option.value !== currentType; }); if (currentType) { select.value = currentType; return; } var selected = select.options[select.selectedIndex]; if (!selected || selected.disabled) { var first = select.querySelector('[data-field-type-option][data-enabled="1"]'); select.value = first ? first.value : ''; } }, ajax: function (url, data, success, error) { Adminx.Loader.show(); fetch(url, { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' } }) .then(function (res) { return res.json().then(function (json) { if (!res.ok || json.success === false) { var err = new Error(json.message || 'Ошибка запроса'); err.payload = json; throw err; } return json; }); }) .then(function (json) { if (success) { success(json); } }) .catch(function (err) { if (error) { error(err.payload || { message: err.message }); } else { Adminx.Toast.show(err.message || 'Ошибка запроса', 'error'); } }) .finally(function () { Adminx.Loader.hide(); }); }, fillRubricNew: function () { this.clearErrors(this.form); this.form.reset(); this.form.elements.id.value = ''; this.form.elements.rubric_template_id.value = '1'; var presetWrap = this.form.querySelector('[data-rubric-preset-wrap]'); if (presetWrap) { presetWrap.hidden = false; } if (this.form.elements.rubric_preset) { this.form.elements.rubric_preset.value = ''; } this.syncRubricPresetHint(); this.setCodeValue(this.form.elements.rubric_code_start, ''); this.setCodeValue(this.form.elements.rubric_code_end, ''); this.setCodeValue(this.form.elements.rubric_start_code, ''); if (this.form.elements.rubric_template_id.selectedIndex < 0) { this.form.elements.rubric_template_id.selectedIndex = 0; } document.getElementById('rubricDrawerTitle').textContent = 'Новая рубрика'; this.setHint('[data-rubric-alias-state]', ''); this.updateRubricAliasPreview(); this.refreshEditors(); }, fillRubricEdit: function (row) { if (!row) { return; } var self = this; this.clearErrors(this.form); fetch(this.base() + '/rubrics/' + row.getAttribute('data-id'), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var item = json.data || {}; self.form.elements.id.value = item.Id || ''; self.form.elements.rubric_title.value = item.rubric_title || ''; self.form.elements.rubric_alias.value = item.rubric_alias || ''; var presetWrap = self.form.querySelector('[data-rubric-preset-wrap]'); if (presetWrap) { presetWrap.hidden = true; } self.form.elements.rubric_template_id.value = item.rubric_template_id || 1; if (self.form.elements.rubric_template_id.selectedIndex < 0) { self.form.elements.rubric_template_id.selectedIndex = 0; } self.form.elements.rubric_docs_active.value = String(item.rubric_docs_active || 0); self.form.elements.rubric_meta_gen.value = String(item.rubric_meta_gen || 0); self.form.elements.rubric_alias_history.value = String(item.rubric_alias_history || 0); self.form.elements.rubric_description.value = item.rubric_description || ''; self.setCodeValue(self.form.elements.rubric_code_start, item.rubric_code_start || ''); self.setCodeValue(self.form.elements.rubric_code_end, item.rubric_code_end || ''); self.setCodeValue(self.form.elements.rubric_start_code, item.rubric_start_code || ''); document.getElementById('rubricDrawerTitle').textContent = 'Редактирование рубрики #' + item.Id; self.setHint('[data-rubric-alias-state]', ''); self.updateRubricAliasPreview(); self.refreshEditors(); }); }, submitRubric: function () { var self = this; var id = this.form.elements.id.value; this.saveEditors(); this.clearErrors(this.form); this.checkRubricAlias(true, function (ok) { if (!ok) { return; } self.ajax(self.base() + '/rubrics' + (id ? '/' + id : ''), new FormData(self.form), function (json) { Adminx.Toast.show(json.message || 'Рубрика сохранена', 'success'); if (Adminx.Drawer) { Adminx.Drawer.close(); } self.applyFilterUrl(window.location.href, false); }, function (json) { self.showErrors(self.form, json); }); }); }, syncRubricPresetHint: function () { if (!this.form || !this.form.elements.rubric_preset) { return; } var select = this.form.elements.rubric_preset; var option = select.options[select.selectedIndex]; var hint = this.form.querySelector('[data-rubric-preset-hint]'); if (hint) { hint.textContent = option ? option.getAttribute('data-description') || '' : ''; } }, updateRubricAliasPreview: function () { if (!this.form) { return; } var output = this.form.querySelector('[data-rubric-alias-preview]'); if (!output) { return; } var pattern = String(this.form.elements.rubric_alias.value || '').replace(/^\/+|\/+$/g, ''); var now = new Date(); var pad = function (value) { return String(value).padStart(2, '0'); }; var replacements = { '%d': pad(now.getDate()), '%m': pad(now.getMonth() + 1), '%Y': String(now.getFullYear()), '%y': String(now.getFullYear()).slice(-2), '%H': pad(now.getHours()), '%M': pad(now.getMinutes()), '%S': pad(now.getSeconds()) }; Object.keys(replacements).forEach(function (token) { pattern = pattern.split(token).join(replacements[token]); }); output.textContent = '/' + (pattern ? pattern + '/' : '') + 'document-alias'; }, checkRubricAlias: function (required, done) { var alias = this.form.elements.rubric_alias.value.trim(); var id = this.form.elements.id.value || 0; if (!alias && !required) { this.setHint('[data-rubric-alias-state]', ''); if (done) { done(false); } return; } var self = this; fetch(this.base() + '/rubrics/alias-check?alias=' + encodeURIComponent(alias) + '&id=' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var data = json.data || {}; self.setHint('[data-rubric-alias-state]', data.message || '', data.valid && data.available); if (done) { done(!!(data.valid && data.available)); } }); }, deleteRubric: function (row) { if (!row || !window.confirm('Удалить рубрику «' + row.getAttribute('data-title') + '»?')) { return; } var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); var self = this; this.ajax(this.base() + '/rubrics/' + row.getAttribute('data-id') + '/delete', data, function () { Adminx.Toast.show('Рубрика удалена', 'success'); self.applyFilterUrl(window.location.href, false); }); }, openAdminView: function (row) { if (!row) { return; } this.currentRubricId = parseInt(row.getAttribute('data-id'), 10) || 0; this.adminViewSelected = ''; this.loadAdminView(); }, loadAdminView: function () { var self = this; if (!this.currentRubricId || !this.adminViewForm) { return; } Adminx.Loader.show(); fetch(this.base() + '/rubrics/' + this.currentRubricId + '/admin-view', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (res) { return res.json().then(function (json) { if (!res.ok || json.success === false) { throw new Error(json.message || 'Не удалось загрузить представление'); } return json.data || {}; }); }).then(function (data) { var rubric = data.rubric || {}; self.adminViewFields = data.fields || []; self.adminViewPreview = data.preview || []; self.adminViewState = data.view || { mode: 'standard', items: [], template: '' }; self.adminViewState.items = Array.isArray(self.adminViewState.items) ? self.adminViewState.items : []; self.adminViewState.template = self.adminViewState.template || ''; self.adminViewForm.elements.rubric_id.value = self.currentRubricId; self.adminViewForm.elements.template.value = self.adminViewState.template; document.getElementById('rubricAdminViewTitle').textContent = 'Вид документов: ' + (rubric.rubric_title || ''); var subtitle = document.querySelector('[data-admin-view-subtitle]'); if (subtitle) { subtitle.textContent = 'Рубрика #' + self.currentRubricId + ' · настройка действует только в Adminx.'; } var documentsLink = document.querySelector('[data-admin-view-documents]'); if (documentsLink) { documentsLink.href = self.base() + '/documents?rubric_id=' + self.currentRubricId; } self.renderAdminView(); }).catch(function (error) { Adminx.Toast.show(error.message || 'Не удалось загрузить представление', 'error'); }).finally(function () { Adminx.Loader.hide(); }); }, renderAdminView: function () { if (!this.adminViewState) { return; } var self = this; document.querySelectorAll('[data-admin-view-mode]').forEach(function (button) { button.setAttribute('aria-pressed', button.getAttribute('data-admin-view-mode') === self.adminViewState.mode ? 'true' : 'false'); }); var builder = document.querySelector('[data-admin-view-builder]'); if (builder) { builder.classList.toggle('is-standard', this.adminViewState.mode === 'standard'); } var custom = document.querySelector('[data-admin-view-custom]'); if (custom) { custom.hidden = this.adminViewState.mode !== 'custom'; } this.renderAdminViewFields(''); this.renderAdminViewItems(); this.renderAdminViewInspector(); this.renderAdminViewTokens(); this.renderAdminViewPreview(); }, renderAdminViewFields: function (search) { var root = document.querySelector('[data-admin-view-fields]'); if (!root) { return; } var needle = String(search || '').trim().toLowerCase(); var selected = {}; (this.adminViewState ? this.adminViewState.items : []).forEach(function (item) { selected[item.source] = true; }); var fields = this.adminViewFields.filter(function (field) { return !needle || (field.title + ' ' + field.alias + ' ' + field.type).toLowerCase().indexOf(needle) !== -1; }); root.innerHTML = fields.length ? fields.map(function (field) { var used = !!selected[field.source]; return ''; }).join('') : '
Поля не найдены
'; var count = document.querySelector('[data-admin-view-field-count]'); if (count) { count.textContent = this.adminViewFields.length; } }, renderAdminViewItems: function () { var root = document.querySelector('[data-admin-view-items]'); if (!root || !this.adminViewState) { return; } var self = this; var items = this.adminViewState.items || []; root.innerHTML = items.length ? items.map(function (item, index) { return '
' + '' + '' + (index + 1) + '' + '' + esc(item.label) + '' + esc(item.type || '') + ' · ' + esc(item.format || 'text') + '' + '' + esc(item.width || 'medium') + '' + '' + '' + '' + '' + '
'; }).join('') : '
Поля не добавленыВыберите их в левой колонке. Название, состояние и действия документа останутся системными.
'; var count = document.querySelector('[data-admin-view-item-count]'); if (count) { count.textContent = items.length + ' / 10'; } }, renderAdminViewInspector: function () { var empty = document.querySelector('[data-admin-view-inspector-empty]'); var form = document.querySelector('[data-admin-view-inspector]'); var item = this.adminViewItem(this.adminViewSelected); if (empty) { empty.hidden = !!item; } if (form) { form.hidden = !item; } if (!item || !form) { return; } form.querySelector('[data-admin-view-label]').value = item.label || ''; form.querySelector('[data-admin-view-format]').value = item.format || 'text'; var width = form.querySelector('input[name="admin_view_width"][value="' + (item.width || 'medium') + '"]'); if (width) { width.checked = true; } var field = this.adminViewField(item.source); var meta = form.querySelector('[data-admin-view-field-meta]'); if (meta && field) { meta.innerHTML = '' + esc(field.alias || 'без алиаса') + '' + esc(field.type || '') + '[field:' + esc(field.alias || field.id) + ']'; } }, renderAdminViewTokens: function () { var root = document.querySelector('[data-admin-view-tokens]'); if (!root || !this.adminViewState) { return; } var tokens = [ { token: '[document:title]', label: 'Название' }, { token: '[document:id]', label: 'ID' }, { token: '[document:alias]', label: 'Alias' } ]; this.adminViewState.items.forEach(function (item) { tokens.push({ token: '[field:' + (item.alias || item.field_id) + ']', label: item.label }); }); root.innerHTML = tokens.map(function (item) { return ''; }).join(''); }, renderAdminViewPreview: function () { var root = document.querySelector('[data-admin-view-preview]'); if (!root || !this.adminViewState) { return; } var count = document.querySelector('[data-admin-view-preview-count]'); if (count) { count.textContent = this.adminViewPreview.length; } if (this.adminViewState.mode === 'standard') { root.innerHTML = '
Стандартный список документовID, название, рубрика, состояние, дата и системные действия.
'; return; } if (!this.adminViewPreview.length) { root.innerHTML = '
В рубрике пока нет документов
'; return; } var self = this; if (this.adminViewState.mode === 'table') { root.innerHTML = '
' + this.adminViewState.items.map(function (item) { return ''; }).join('') + '' + this.adminViewPreview.map(function (document) { return '' + self.adminViewState.items.map(function (item) { return ''; }).join('') + ''; }).join('') + '
Документ' + esc(item.label) + '
' + esc(document.document_title) + '#' + esc(document.Id) + '' + self.adminViewValueHtml(document, item) + '
'; return; } root.innerHTML = '
' + this.adminViewPreview.map(function (document) { var content = self.adminViewState.mode === 'custom' ? self.adminViewCustomHtml(document) : '
' + self.adminViewState.items.map(function (item) { return '
' + esc(item.label) + '' + self.adminViewValueHtml(document, item) + '
'; }).join('') + '
'; return '
' + esc(document.document_title) + '#' + esc(document.Id) + ' · ' + esc(document.document_alias || 'без alias') + '
' + content + '
'; }).join('') + '
'; }, adminViewValueHtml: function (document, item) { var values = document.admin_field_values || {}; var value = values[item.source] || { text: '', image: '' }; if (item.format === 'image') { return value.image ? '' : ''; } if (item.format === 'badge') { return '' + esc(value.text || '—') + ''; } return '' + esc(value.text || '—') + ''; }, adminViewCustomHtml: function (document) { var template = this.adminViewForm ? this.adminViewForm.elements.template.value : ''; if (!template) { template = '[document:title]#[document:id] · [document:alias]'; } var replace = { '[document:id]': String(document.Id || ''), '[document:title]': esc(document.document_title || ''), '[document:alias]': esc(document.document_alias || '') }; var values = document.admin_field_values || {}; (this.adminViewState.items || []).forEach(function (item) { var value = values[item.source] || { text: '' }; replace['[field:' + item.field_id + ']'] = esc(value.text || ''); if (item.alias) { replace['[field:' + item.alias + ']'] = esc(value.text || ''); } }); Object.keys(replace).forEach(function (token) { template = template.split(token).join(replace[token]); }); var parsed = new DOMParser().parseFromString('
' + template + '
', 'text/html'); parsed.querySelectorAll('script,style,iframe,object,embed,form').forEach(function (node) { node.remove(); }); parsed.querySelectorAll('*').forEach(function (node) { Array.prototype.slice.call(node.attributes).forEach(function (attribute) { if (/^on/i.test(attribute.name) || /javascript\s*:/i.test(attribute.value)) { node.removeAttribute(attribute.name); } }); }); return parsed.body.firstElementChild ? parsed.body.firstElementChild.innerHTML : ''; }, setAdminViewMode: function (mode) { if (!this.adminViewState || ['standard', 'table', 'cards', 'custom'].indexOf(mode) === -1) { return; } this.adminViewState.mode = mode; this.renderAdminView(); }, addAdminViewItem: function (source) { if (!this.adminViewState || this.adminViewState.items.length >= 10 || this.adminViewItem(source)) { if (this.adminViewState && this.adminViewState.items.length >= 10) { Adminx.Toast.show('Можно добавить не больше 10 полей', 'warning'); } return; } var field = this.adminViewField(source); if (!field) { return; } this.adminViewState.items.push({ source: field.source, field_id: field.id, label: field.title, format: field.format || 'text', width: 'medium', type: field.type, alias: field.alias || '' }); this.adminViewSelected = source; if (this.adminViewState.mode === 'standard') { this.adminViewState.mode = 'table'; } this.renderAdminView(); }, removeAdminViewItem: function (source) { if (!this.adminViewState) { return; } this.adminViewState.items = this.adminViewState.items.filter(function (item) { return item.source !== source; }); if (this.adminViewSelected === source) { this.adminViewSelected = ''; } this.renderAdminView(); }, selectAdminViewItem: function (source) { this.adminViewSelected = source || ''; this.renderAdminViewItems(); this.renderAdminViewInspector(); }, captureAdminViewInspector: function () { var item = this.adminViewItem(this.adminViewSelected); var form = document.querySelector('[data-admin-view-inspector]'); if (!item || !form) { return; } item.label = form.querySelector('[data-admin-view-label]').value.trim() || (this.adminViewField(item.source) || {}).title || item.label; item.format = form.querySelector('[data-admin-view-format]').value || 'text'; var width = form.querySelector('input[name="admin_view_width"]:checked'); item.width = width ? width.value : 'medium'; this.renderAdminViewItems(); this.renderAdminViewTokens(); this.renderAdminViewPreview(); }, moveAdminViewItem: function (source, direction) { if (!this.adminViewState || !direction) { return; } var index = this.adminViewState.items.findIndex(function (item) { return item.source === source; }); var target = index + direction; if (index < 0 || target < 0 || target >= this.adminViewState.items.length) { return; } var item = this.adminViewState.items.splice(index, 1)[0]; this.adminViewState.items.splice(target, 0, item); this.renderAdminView(); }, adminViewDragStart: function (event) { var row = event.target.closest('[data-admin-view-item]'); if (!row) { return; } this.adminViewDragSource = row.getAttribute('data-source') || ''; row.classList.add('is-dragging'); if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', this.adminViewDragSource); } }, adminViewDragOver: function (event) { var target = event.target.closest('[data-admin-view-item]'); if (!target || target.getAttribute('data-source') === this.adminViewDragSource) { return; } event.preventDefault(); document.querySelectorAll('[data-admin-view-item]').forEach(function (item) { item.classList.remove('is-drop-before', 'is-drop-after'); }); var rect = target.getBoundingClientRect(); target.classList.add(event.clientY < rect.top + rect.height / 2 ? 'is-drop-before' : 'is-drop-after'); }, adminViewDrop: function (event) { var target = event.target.closest('[data-admin-view-item]'); if (!target || !this.adminViewState) { this.adminViewDragEnd(); return; } event.preventDefault(); var sourceIndex = this.adminViewState.items.findIndex(function (item) { return item.source === this.adminViewDragSource; }, this); var targetSource = target.getAttribute('data-source'); var targetIndex = this.adminViewState.items.findIndex(function (item) { return item.source === targetSource; }); if (sourceIndex >= 0 && targetIndex >= 0 && sourceIndex !== targetIndex) { var moved = this.adminViewState.items.splice(sourceIndex, 1)[0]; if (sourceIndex < targetIndex) { targetIndex -= 1; } var rect = target.getBoundingClientRect(); if (event.clientY >= rect.top + rect.height / 2) { targetIndex += 1; } this.adminViewState.items.splice(targetIndex, 0, moved); } this.adminViewDragEnd(); this.renderAdminView(); }, adminViewDragEnd: function () { this.adminViewDragSource = ''; document.querySelectorAll('[data-admin-view-item]').forEach(function (item) { item.classList.remove('is-dragging', 'is-drop-before', 'is-drop-after'); }); }, setAdminViewTemplateExample: function () { if (!this.adminViewForm) { return; } this.adminViewForm.elements.template.value = '
\n [document:title]\n #[document:id] · [document:alias]\n
'; this.renderAdminViewPreview(); }, insertAdminViewToken: function (token) { if (!this.adminViewForm) { return; } var textarea = this.adminViewForm.elements.template; var start = textarea.selectionStart || 0; var end = textarea.selectionEnd || 0; textarea.value = textarea.value.slice(0, start) + token + textarea.value.slice(end); textarea.focus(); textarea.setSelectionRange(start + token.length, start + token.length); this.renderAdminViewPreview(); }, saveAdminView: function () { if (!this.adminViewState || !this.adminViewForm) { return; } var data = new FormData(this.adminViewForm); data.append('mode', this.adminViewState.mode || 'standard'); data.append('items', JSON.stringify(this.adminViewState.items || [])); var self = this; this.ajax(this.base() + '/rubrics/' + this.currentRubricId + '/admin-view', data, function (json) { if (json.data && json.data.view) { self.adminViewState = json.data.view; } Adminx.Toast.show(json.message || 'Представление документов сохранено', 'success'); self.renderAdminView(); }); }, adminViewField: function (source) { return this.adminViewFields.find(function (field) { return field.source === source; }) || null; }, adminViewItem: function (source) { if (!this.adminViewState || !source) { return null; } return this.adminViewState.items.find(function (item) { return item.source === source; }) || null; }, openTemplates: function (row) { if (!row) { return; } this.closeTemplateTagPalette(); this.currentRubricId = parseInt(row.getAttribute('data-id'), 10) || 0; this.activateTemplateTab('main'); this.loadTemplates(); }, loadTemplates: function () { var self = this; if (!this.currentRubricId) { return; } fetch(this.base() + '/rubrics/' + this.currentRubricId + '/templates', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var data = json.data || {}; var rubric = data.rubric || {}; self.currentTemplateFields = data.fields || []; document.getElementById('rubricTemplatesTitle').textContent = 'Шаблоны: ' + (rubric.rubric_title || ''); var sub = document.querySelector('[data-rubric-templates-subtitle]'); if (sub) { sub.textContent = 'Рубрика #' + self.currentRubricId + ', ' + (data.templates || []).length + ' дополнительных шаблонов.'; } self.fillMainTemplateForm(rubric); self.renderTemplateTags(data.fields || []); self.renderExtraTemplates(data.templates || []); self.refreshEditors(); }) .catch(function () { Adminx.Toast.show('Не удалось загрузить шаблоны рубрики', 'error'); }); }, fillMainTemplateForm: function (rubric) { if (!this.templateForm) { return; } this.templateForm.elements.rubric_id.value = this.currentRubricId; this.setCodeValue(this.templateForm.elements.rubric_header_template, rubric.rubric_header_template || ''); this.setCodeValue(this.templateForm.elements.rubric_og_template, rubric.rubric_og_template || ''); this.setCodeValue(this.templateForm.elements.rubric_template, rubric.rubric_template || ''); this.setCodeValue(this.templateForm.elements.rubric_footer_template, rubric.rubric_footer_template || ''); this.setCodeValue(this.templateForm.elements.rubric_teaser_template, rubric.rubric_teaser_template || ''); this.activeTemplateTextarea = this.templateForm.elements.rubric_template; }, setOpenGraphExample: function () { var textarea = this.templateForm ? this.templateForm.elements.rubric_og_template : null; var self = this; var markup = '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + ''; if (!textarea) { return; } var current = textarea._adminxCodeMirror ? textarea._adminxCodeMirror.getValue() : textarea.value; if (String(current || '').trim() === '') { this.setCodeValue(textarea, markup); this.activeTemplateTextarea = textarea; return; } Adminx.Confirm.open({ kind: 'warning', title: 'Заменить OG-разметку?', message: 'Текущий код в этой вкладке будет заменён базовой разметкой.', confirmLabel: 'Заменить', onConfirm: function () { self.setCodeValue(textarea, markup); self.activeTemplateTextarea = textarea; } }); }, submitMainTemplate: function () { var self = this; this.saveEditors(); this.ajax(this.base() + '/rubrics/' + this.currentRubricId + '/templates/main', new FormData(this.templateForm), function () { Adminx.Toast.show('Шаблоны рубрики сохранены', 'success'); self.loadTemplates(); }); }, activateTemplateTab: function (name) { name = name || 'main'; this.closeTemplateTagPalette(); document.querySelectorAll('[data-rubric-template-tab]').forEach(function (tab) { tab.setAttribute('aria-selected', tab.getAttribute('data-rubric-template-tab') === name ? 'true' : 'false'); }); document.querySelectorAll('[data-rubric-template-panel]').forEach(function (panel) { panel.classList.toggle('active', panel.getAttribute('data-rubric-template-panel') === name); }); this.refreshEditors(); }, renderTemplateTags: function (fields) { var root = document.querySelector('[data-rubric-template-tags]'); var count = document.querySelector('[data-rubric-template-tags-count]'); if (!root) { return; } if (count) { count.textContent = String(fields.length); } if (!fields.length) { root.innerHTML = '
Поля не найдены.
'; return; } root.innerHTML = fields.map(function (field) { return ''; }).join(''); }, openTemplateTagPalette: function (field) { var palette = document.querySelector('[data-rubric-template-tags-panel]'); 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; } this.activeTemplateTextarea = textarea; this.loadTemplateRegistryTags(); field.insertBefore(palette, anchor || field.firstChild); palette.hidden = false; search = palette.querySelector('[data-rubric-template-tags-search]'); if (search) { search.value = ''; this.filterTemplateTags(''); search.focus(); } }, loadTemplateRegistryTags: function () { var self = this; if (this.templateRegistryLoaded || this.templateRegistryLoading) { return; } this.templateRegistryLoading = true; fetch(this.base() + '/rubrics/template-tags', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { self.renderTemplateRegistryTags((json.data && json.data.groups) || []); self.templateRegistryLoaded = true; }) .catch(function () { Adminx.Toast.show('Не удалось загрузить реестр тегов', 'error'); }) .finally(function () { self.templateRegistryLoading = false; }); }, renderTemplateRegistryTags: function (groups) { var nav = document.querySelector('[data-rubric-template-tag-nav]'); var root = document.querySelector('[data-rubric-template-tag-groups]'); if (!nav || !root) { return; } groups.forEach(function (group, index) { var items = group && Array.isArray(group.items) ? group.items : []; if (!items.length) { return; } var name = 'registry-' + index; nav.insertAdjacentHTML('beforeend', ''); var buttons = items.map(function (tag) { var select = tag.select ? ' data-rubric-template-tag-select="' + esc(tag.select) + '"' : ''; return ''; }).join(''); root.querySelector('[data-rubric-template-tags-empty]').insertAdjacentHTML('beforebegin', '
' + '
' + buttons + '
'); }); }, closeTemplateTagPalette: function () { var palette = document.querySelector('[data-rubric-template-tags-panel]'); if (palette) { palette.hidden = true; } }, activateTemplateTagGroup: function (name) { var palette = document.querySelector('[data-rubric-template-tags-panel]'); if (!palette) { return; } palette.querySelectorAll('[data-rubric-template-tag-tab]').forEach(function (tab) { tab.classList.toggle('is-active', tab.getAttribute('data-rubric-template-tag-tab') === String(name)); }); palette.querySelectorAll('[data-rubric-template-tag-panel]').forEach(function (group) { group.classList.toggle('is-active', group.getAttribute('data-rubric-template-tag-panel') === String(name)); }); }, filterTemplateTags: function (query) { var palette = document.querySelector('[data-rubric-template-tags-panel]'); var shown = 0; if (!palette) { return; } query = String(query || '').trim().toLowerCase(); palette.classList.toggle('is-searching', query !== ''); palette.querySelectorAll('[data-rubric-template-tag]').forEach(function (button) { var visible = query === '' || button.textContent.toLowerCase().indexOf(query) !== -1; button.hidden = !visible; if (visible) { shown++; } }); palette.querySelectorAll('[data-rubric-template-tag-group]').forEach(function (group) { group.hidden = query !== '' && !group.querySelector('[data-rubric-template-tag]:not([hidden])'); }); var empty = palette.querySelector('[data-rubric-template-tags-empty]'); if (empty) { empty.hidden = shown > 0; } }, renderExtraTemplates: function (templates) { var root = document.querySelector('[data-extra-template-list]'); var count = document.querySelector('[data-extra-template-count]'); if (count) { count.textContent = String(templates.length); } if (!root) { return; } if (!templates.length) { root.innerHTML = '
Дополнительные шаблоны не созданы.
'; return; } root.innerHTML = templates.map(function (item) { return '
' + '
' + esc(item.title) + 'Шаблон #' + item.id + '
' + '
' + esc(item.created_label) + '' + esc(item.size_label) + '' + item.docs_count + ' документов
' + '
' + '' + (item.can_delete ? '' : '') + '
'; }).join(''); }, fillExtraTemplateNew: function () { this.clearErrors(this.extraTemplateForm); this.extraTemplateForm.reset(); this.extraTemplateForm.elements.id.value = ''; this.extraTemplateForm.elements.rubric_id.value = this.currentRubricId; this.setCodeValue(this.extraTemplateForm.elements.template, ''); document.getElementById('rubricExtraTemplateTitle').textContent = 'Новый дополнительный шаблон'; this.activeTemplateTextarea = this.extraTemplateForm.elements.template; if (Adminx.Drawer) { Adminx.Drawer.open('rubricExtraTemplateDrawer'); } this.refreshEditors(); }, fillExtraTemplateEdit: function (row) { if (!row) { return; } var self = this; this.clearErrors(this.extraTemplateForm); fetch(this.base() + '/rubrics/templates/' + row.getAttribute('data-id'), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var item = json.data || {}; self.extraTemplateForm.elements.id.value = item.id || ''; self.extraTemplateForm.elements.rubric_id.value = item.rubric_id || self.currentRubricId; self.extraTemplateForm.elements.title.value = item.title || ''; self.setCodeValue(self.extraTemplateForm.elements.template, item.template || ''); document.getElementById('rubricExtraTemplateTitle').textContent = 'Редактирование шаблона #' + item.id; self.activeTemplateTextarea = self.extraTemplateForm.elements.template; self.refreshEditors(); }); }, submitExtraTemplate: function () { var self = this; var id = this.extraTemplateForm.elements.id.value; var rubricId = this.extraTemplateForm.elements.rubric_id.value || this.currentRubricId; this.clearErrors(this.extraTemplateForm); this.saveEditors(); this.ajax(this.base() + (id ? '/rubrics/templates/' + id : '/rubrics/' + rubricId + '/templates'), new FormData(this.extraTemplateForm), function () { Adminx.Toast.show('Дополнительный шаблон сохранён', 'success'); if (Adminx.Drawer) { Adminx.Drawer.close('rubricExtraTemplateDrawer'); } self.loadTemplates(); self.applyFilterUrl(window.location.href, false); }, function (json) { self.showErrors(self.extraTemplateForm, json); }); }, deleteExtraTemplate: function (row) { if (!row || !window.confirm('Удалить дополнительный шаблон «' + row.getAttribute('data-title') + '»?')) { return; } var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); var self = this; this.ajax(this.base() + '/rubrics/templates/' + row.getAttribute('data-id') + '/delete', data, function () { Adminx.Toast.show('Дополнительный шаблон удалён', 'success'); self.loadTemplates(); self.applyFilterUrl(window.location.href, false); }); }, insertTemplateTag: function (source) { var textarea = this.visibleTemplateTextarea() || this.activeTemplateTextarea || (this.templateForm && this.templateForm.elements.rubric_template); var tag = typeof source === 'string' ? source : source.getAttribute('data-rubric-template-tag'); var select = typeof source === 'string' ? '' : (source.getAttribute('data-rubric-template-tag-select') || ''); var cm; if (!textarea || !tag) { return; } cm = textarea._adminxCodeMirror; if (cm) { var start = cm.indexFromPos(cm.getCursor()); cm.replaceSelection(tag, 'around'); cm.focus(); if (select && tag.indexOf(select) !== -1) { cm.setSelection(cm.posFromIndex(start + tag.indexOf(select)), cm.posFromIndex(start + tag.indexOf(select) + select.length)); } else { cm.setCursor(cm.posFromIndex(start + tag.length)); } cm.save(); this.closeTemplateTagPalette(); return; } var start = textarea.selectionStart || 0; var end = textarea.selectionEnd || 0; textarea.value = textarea.value.slice(0, start) + tag + textarea.value.slice(end); if (select && tag.indexOf(select) !== -1) { textarea.selectionStart = start + tag.indexOf(select); textarea.selectionEnd = textarea.selectionStart + select.length; } else { textarea.selectionStart = textarea.selectionEnd = start + tag.length; } textarea.focus(); this.closeTemplateTagPalette(); }, setCodeLintResult: function (el, message, kind) { if (!el) { return; } el.textContent = message || ''; el.classList.remove('is-ok', 'is-error'); if (kind) { el.classList.add(kind === 'error' ? 'is-error' : 'is-ok'); } }, lintCodeField: function (field) { if (!field) { return; } var textarea = field.querySelector('textarea[data-code-editor]'); if (!textarea) { return; } if (textarea._adminxCodeMirror) { textarea._adminxCodeMirror.save(); } var result = field.querySelector('[data-rubric-code-lint-result]'); this.setCodeLintResult(result, 'Проверка...', ''); var fd = new FormData(); fd.set('_csrf', Adminx.csrf()); fd.set('code', textarea.value || ''); var self = this; Adminx.Ajax.post(this.base() + '/rubrics/template/lint', fd).then(function (payload) { var d = payload.data || {}; if (d.success) { var okMsg = (d.data && d.data.message) || d.message || 'Синтаксис PHP без ошибок.'; self.setCodeLintResult(result, okMsg, 'ok'); Adminx.Toast.show(okMsg, 'success'); return; } var detail = d.errors && d.errors.code ? d.errors.code : (d.message || 'В PHP-коде есть ошибка.'); self.setCodeLintResult(result, detail, 'error'); Adminx.Toast.show(d.message || 'В PHP-коде есть ошибка.', 'error'); }).catch(function () { self.setCodeLintResult(result, 'Не удалось выполнить проверку.', 'error'); Adminx.Toast.show('Ошибка сети', 'error'); }); }, toggleCodeFieldFullscreen: function (field, force) { if (!field) { return; } var next = typeof force === 'boolean' ? force : !field.classList.contains('is-fullscreen'); // Одновременно раскрыт только один редактор. document.querySelectorAll('.rubrics-code-field.is-fullscreen').forEach(function (other) { if (other !== field) { other.classList.remove('is-fullscreen'); } }); field.classList.toggle('is-fullscreen', next); document.body.classList.toggle('rubrics-code-fullscreen-open', next); var button = field.querySelector('[data-rubric-code-fullscreen]'); if (button) { button.setAttribute('data-tooltip', next ? 'Свернуть редактор' : 'Развернуть редактор'); button.setAttribute('aria-label', next ? 'Свернуть редактор' : 'Развернуть редактор'); button.innerHTML = next ? '' : ''; } var textarea = field.querySelector('textarea[data-code-editor]'); var cm = textarea ? textarea._adminxCodeMirror : null; if (cm) { var height = parseInt(textarea.getAttribute('data-height'), 10) || 360; setTimeout(function () { cm.refresh(); cm.setSize('100%', next ? '100%' : height); }, 40); } }, closeCodeFullscreen: function () { var field = document.querySelector('.rubrics-code-field.is-fullscreen'); if (field) { this.toggleCodeFieldFullscreen(field, false); } }, captureCodeMirror: function (wrapper) { if (!window.Adminx || !Adminx.CodeEditor || !wrapper) { return; } Adminx.CodeEditor.instances.forEach(function (editor) { if (editor.getWrapperElement && editor.getWrapperElement() === wrapper) { var textarea = editor.getTextArea(); if (textarea && textarea.closest('#rubricTemplateForm, #rubricExtraTemplateForm')) { Adminx.Rubrics.activeTemplateTextarea = textarea; } if (textarea && textarea.closest('#rubricFieldForm')) { Adminx.Rubrics.activeFieldTemplateTextarea = textarea; } } }); }, visibleTemplateTextarea: function () { if (this.activeTemplateTextarea && this.activeTemplateTextarea.name && this.isCodeTextareaVisible(this.activeTemplateTextarea)) { return this.activeTemplateTextarea; } var panel = document.querySelector('[data-rubric-template-panel].active'); var textarea; if (panel) { if (panel.getAttribute('data-rubric-template-panel') === 'main' && this.templateForm) { textarea = this.templateForm.elements.rubric_template; if (textarea) { return textarea; } } textarea = panel.querySelector('textarea[data-code-editor]'); if (textarea) { return textarea; } } if (this.extraTemplateForm && !document.getElementById('rubricExtraTemplateDrawer').hidden) { return this.extraTemplateForm.elements.template; } return null; }, isCodeTextareaVisible: function (textarea) { var wrapper; if (!textarea) { return false; } if (textarea._adminxCodeMirror) { wrapper = textarea._adminxCodeMirror.getWrapperElement(); return !!(wrapper && wrapper.offsetParent !== null); } return textarea.offsetParent !== null; }, setCodeValue: function (textarea, value) { if (!textarea) { return; } textarea.value = value || ''; if (textarea._adminxCodeMirror) { textarea._adminxCodeMirror.setValue(value || ''); textarea._adminxCodeMirror.save(); } }, saveEditors: function () { if (!window.Adminx || !Adminx.CodeEditor) { return; } Adminx.CodeEditor.instances.forEach(function (editor) { editor.save(); }); }, refreshEditors: function () { if (!window.Adminx || !Adminx.CodeEditor) { return; } setTimeout(function () { Adminx.CodeEditor.refreshAll(); }, 80); }, openFields: function (row) { if (!row) { return; } this.currentRubricId = parseInt(row.getAttribute('data-id'), 10) || 0; this.loadFields(); }, loadFields: function () { var self = this; fetch(this.base() + '/rubrics/' + this.currentRubricId + '/fields', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var data = json.data || {}; self.currentGroups = data.groups || []; self.currentFieldSetLinks = data.field_set_links || []; self.linkedFieldSetsAvailable = !!data.linked_field_sets_available; if (!self.linkedFieldSetsAvailable) { self.fieldSetMode = 'copy'; } self.currentFields = []; (data.grouped_fields || []).forEach(function (group) { (group.items || []).forEach(function (field) { self.currentFields.push(field); }); }); (data.available_fields || []).forEach(function (field) { self.currentFields.push(field); }); document.getElementById('rubricFieldsTitle').textContent = 'Поля: ' + ((data.rubric || {}).rubric_title || ''); var sub = document.querySelector('[data-rubric-fields-subtitle]'); if (sub) { sub.textContent = 'Рубрика #' + self.currentRubricId + ' · ' + self.currentFields.length + ' полей · форма редактора документа'; } var conditionsToggle = document.querySelector('[data-builder-conditions-enabled]'); if (conditionsToggle) { conditionsToggle.checked = !!((data.rubric || {}).form_conditions_enabled); } self.syncBuilderRuntime(); self.builderDrafts = {}; self.builderSelectedId = 0; self.setBuilderDirty(false); self.renderGroups(data.grouped_fields || []); self.syncBuilderFieldSet(); self.renderLinkedFieldSets(); self.fillGroupSelect(); if (Adminx.Drawer) { Adminx.Drawer.open('rubricFieldsDrawer'); } }); }, renderGroups: function (groups) { var canvas = ''; var palette = ''; var placed = {}; var availableCount = 0; var self = this; var colors = [ ['var(--blue-100)', 'var(--blue-600)'], ['var(--green-100)', 'var(--green-600)'], ['var(--cyan-100)', 'var(--cyan-600)'], ['var(--amber-100)', 'var(--amber-600)'], ['var(--blue-50)', 'var(--blue-700)'] ]; groups.forEach(function (group) { (group.items || []).forEach(function (field) { placed[parseInt(field.Id, 10)] = true; }); }); this.currentFields.forEach(function (field) { if (placed[parseInt(field.Id, 10)]) { return; } availableCount++; palette += self.builderPaletteMarkup(field); }); groups.forEach(function (group, index) { var color = colors[index % colors.length]; var groupCondition = group.condition && typeof group.condition === 'object' ? group.condition : {}; var groupAttrs = group.id ? ' draggable="true" data-group-row data-id="' + group.id + '" data-title="' + esc(group.title) + '" data-description="' + esc(group.description || '') + '" data-condition="' + esc(JSON.stringify(groupCondition)) + '"' : ''; canvas += '
' + '
' + (group.id ? '' : '') + '' + '

' + esc(group.id ? group.title : 'Основные поля') + '

' + esc(group.description || (group.id ? 'Группа полей документа' : 'Поля без отдельной группы')) + '

' + (groupCondition.tree ? 'условие' : '') + '' + (group.items || []).length + '' + (group.id ? '
' : '') + '
'; (group.items || []).forEach(function (field) { canvas += self.builderFieldMarkup(field, group.id); }); if (!(group.items || []).length) { canvas += '
Перетащите поле в эту группу
'; } canvas += '
'; }); var canvasRoot = document.querySelector('[data-builder-canvas]'); var paletteRoot = document.querySelector('[data-builder-palette]'); var count = document.querySelector('[data-builder-field-count]'); if (canvasRoot) { canvasRoot.innerHTML = canvas || '
Добавьте первое поле рубрики.
'; } if (paletteRoot) { paletteRoot.innerHTML = palette || '
Все поля размещеныНовых полей для добавления нет.
'; } if (count) { count.textContent = availableCount; } this.updateBuilderGroupCounts(); }, syncBuilderFieldSet: function () { var select = document.querySelector('[data-builder-field-set]'); var button = document.querySelector('[data-builder-field-set-apply]'); var exportButton = document.querySelector('[data-builder-field-set-export]'); var importButton = document.querySelector('[data-builder-field-set-import]'); var hint = document.querySelector('[data-builder-field-set-hint]'); var option = select && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null; var selectedCode = select ? String(select.value || '') : ''; var alreadyLinked = this.fieldSetMode === 'linked' && (this.currentFieldSetLinks || []).some(function (link) { return String(link.code || '') === selectedCode; }); if (button) { button.disabled = !select || !selectedCode || !this.currentRubricId || alreadyLinked || (this.fieldSetMode === 'linked' && !this.linkedFieldSetsAvailable); } if (exportButton) { exportButton.disabled = !this.currentRubricId || !this.currentFields.length; } if (importButton) { importButton.disabled = !this.currentRubricId; } document.querySelectorAll('[data-field-set-mode="linked"]').forEach(function (modeButton) { modeButton.disabled = !Adminx.Rubrics.linkedFieldSetsAvailable; }); if (hint) { var description = option && select.value ? (option.getAttribute('data-description') || 'Поля будут добавлены в текущую рубрику.') : 'Готовая группа полей без изменения существующих данных.'; if (!this.linkedFieldSetsAvailable) { hint.textContent = 'Связанный режим станет доступен после применения миграций рубрик.'; } else if (alreadyLinked) { hint.textContent = 'Этот набор уже связан с рубрикой. Его обновления находятся ниже.'; } else { hint.textContent = this.fieldSetMode === 'linked' ? description + ' Обновления набора можно будет применить после предварительной проверки.' : description; } } }, setBuilderFieldSetMode: function (mode) { if (mode === 'linked' && !this.linkedFieldSetsAvailable) { Adminx.Toast.show('Сначала примените миграции рубрик', 'warning'); return; } this.fieldSetMode = mode === 'linked' ? 'linked' : 'copy'; document.querySelectorAll('[data-field-set-mode]').forEach(function (button) { button.setAttribute('aria-pressed', button.getAttribute('data-field-set-mode') === Adminx.Rubrics.fieldSetMode ? 'true' : 'false'); }); this.syncBuilderFieldSet(); }, renderLinkedFieldSets: function () { var root = document.querySelector('[data-builder-field-set-links]'); if (!root) { return; } var links = this.currentFieldSetLinks || []; root.hidden = !links.length; if (!links.length) { root.innerHTML = ''; return; } root.innerHTML = '' + links.map(function (link) { var state = !link.available ? 'Источник недоступен' : (link.update_available ? 'Есть обновление' : 'Актуален'); var badge = !link.available ? 'badge-red' : (link.update_available ? 'badge-amber' : 'badge-green'); return '
' + esc(link.title || link.code) + '' + esc(link.code) + '' + '' + state + '' + '
' + (link.available && link.update_available ? '' : '') + '
'; }).join(''); }, applyBuilderFieldSet: function () { var select = document.querySelector('[data-builder-field-set]'); var button = document.querySelector('[data-builder-field-set-apply]'); var code = select ? String(select.value || '') : ''; if (!code || !this.currentRubricId) { return; } if (this.builderDirty) { Adminx.Toast.show('Сначала сохраните изменения конструктора', 'warning'); return; } var self = this; if (button) { button.disabled = true; button.classList.add('is-loading'); } fetch(this.base() + '/rubrics/' + this.currentRubricId + '/field-sets/' + encodeURIComponent(code) + '/preview', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (response) { return response.json(); }).then(function (json) { var preview = json.data || {}; if (!json.success) { throw new Error(json.message || 'Не удалось проверить набор'); } if (preview.conflicts && preview.conflicts.length) { Adminx.Toast.show('Конфликт полей: ' + preview.conflicts.join(', '), 'error'); return; } Adminx.Confirm.open({ kind: 'warning', title: 'Добавить набор «' + (preview.title || code) + '»?', message: 'Будет создано полей: ' + (preview.create || 0) + '. Уже есть и останутся без изменений: ' + (preview.reuse || 0) + '.', confirmLabel: 'Добавить набор', onConfirm: function () { var data = new FormData(); data.append('_csrf', self.form.elements._csrf.value); data.append('mode', self.fieldSetMode); self.ajax(self.base() + '/rubrics/' + self.currentRubricId + '/field-sets/' + encodeURIComponent(code) + '/apply', data, function (result) { Adminx.Toast.show(result.message || 'Набор полей добавлен', 'success'); select.value = ''; self.loadFields(); self.applyFilterUrl(window.location.href, false); }); } }); }).catch(function (error) { Adminx.Toast.show(error.message || 'Не удалось проверить набор', 'error'); }).then(function () { if (button) { button.classList.remove('is-loading'); } self.syncBuilderFieldSet(); }); }, exportBuilderFieldSet: function () { if (!this.currentRubricId || !this.currentFields.length) { return; } if (this.builderDirty) { Adminx.Toast.show('Сначала сохраните изменения конструктора', 'warning'); return; } var button = document.querySelector('[data-builder-field-set-export]'); if (button) { button.disabled = true; button.classList.add('is-loading'); } var self = this; fetch(this.base() + '/rubrics/' + this.currentRubricId + '/field-sets/export', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (response) { return response.json(); }).then(function (json) { if (!json.success) { throw new Error(json.message || 'Не удалось подготовить набор'); } var data = json.data || {}; var blob = new Blob([JSON.stringify(data.descriptor || {}, null, 2) + '\n'], { type: 'application/json;charset=utf-8' }); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = data.filename || ('rubric-' + self.currentRubricId + '-field-set.json'); document.body.appendChild(link); link.click(); link.remove(); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); Adminx.Toast.show(json.message || 'Набор полей экспортирован', 'success'); }).catch(function (error) { Adminx.Toast.show(error.message || 'Не удалось экспортировать набор', 'error'); }).then(function () { if (button) { button.classList.remove('is-loading'); } self.syncBuilderFieldSet(); }); }, openBuilderFieldSetImport: function () { if (!this.currentRubricId) { return; } if (this.builderDirty) { Adminx.Toast.show('Сначала сохраните изменения конструктора', 'warning'); return; } var self = this; var overlay = document.createElement('div'); overlay.className = 'overlay rubrics-field-set-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); var fileInput = overlay.querySelector('[data-field-set-import-file]'); var dropzone = overlay.querySelector('[data-field-set-dropzone]'); var previewRoot = overlay.querySelector('[data-field-set-import-preview]'); var stateRoot = overlay.querySelector('[data-field-set-import-state]'); var applyButton = overlay.querySelector('[data-field-set-import-apply]'); var nameRoot = overlay.querySelector('[data-field-set-import-name]'); var descriptor = ''; var fingerprint = ''; var importMode = self.fieldSetMode; var importHasConflicts = false; var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); }; var showError = function (message) { fingerprint = ''; importHasConflicts = true; applyButton.disabled = true; previewRoot.hidden = false; previewRoot.innerHTML = '
Набор не готов к импорту

' + esc(message) + '

'; stateRoot.hidden = true; }; var preview = function (file) { if (!file) { return; } nameRoot.textContent = file.name || 'field-set.json'; if (file.size > 262144) { showError('Файл превышает 256 КБ'); return; } var reader = new FileReader(); reader.onerror = function () { showError('Не удалось прочитать выбранный файл'); }; reader.onload = function () { descriptor = String(reader.result || ''); stateRoot.hidden = false; stateRoot.classList.add('is-loading'); stateRoot.innerHTML = 'Проверяем наборСравниваем alias и типы полей.'; previewRoot.hidden = true; applyButton.disabled = true; var data = new FormData(); data.append('_csrf', self.form.elements._csrf.value); data.append('descriptor', descriptor); fetch(self.base() + '/rubrics/' + self.currentRubricId + '/field-sets/import/preview', { method: 'POST', body: data, headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (response) { return response.json(); }).then(function (json) { if (!json.success) { throw new Error(json.message || 'Набор не прошёл проверку'); } var result = json.data || {}; fingerprint = result.fingerprint || ''; var conflicts = result.conflicts || []; importHasConflicts = !!conflicts.length; stateRoot.hidden = true; previewRoot.hidden = false; previewRoot.innerHTML = '
' + esc(result.title || 'Набор полей') + '' + esc(result.description || 'Описание не задано') + '
' + '
' + (result.field_count || 0) + 'всего полей' + (result.create || 0) + 'будет создано' + (result.reuse || 0) + 'уже существует' + conflicts.length + 'конфликтов
' + '
После импорта
' + (conflicts.length ? '
Конфликт типов

' + esc(conflicts.join(', ')) + '

' : '
' + (result.source_schema_match ? 'Исходная схема распознана' : 'Режим копии') + '

' + (result.source_schema_match ? 'Поля исходной рубрики будут переиспользованы без дублей.' : 'Созданные поля станут независимой частью текущей рубрики.' + (result.generated_aliases ? ' Для полей без системного имени подготовлено alias: ' + result.generated_aliases + '.' : '')) + '

'); applyButton.disabled = !!conflicts.length || !fingerprint || (!(parseInt(result.create, 10) > 0) && importMode !== 'linked'); }).catch(function (error) { showError(error.message || 'Набор не прошёл проверку'); }).then(function () { stateRoot.classList.remove('is-loading'); }); }; reader.readAsText(file, 'UTF-8'); }; fileInput.addEventListener('change', function () { preview(fileInput.files && fileInput.files[0]); }); dropzone.addEventListener('dragover', function (event) { event.preventDefault(); dropzone.classList.add('is-dragover'); }); dropzone.addEventListener('dragleave', function () { dropzone.classList.remove('is-dragover'); }); dropzone.addEventListener('drop', function (event) { event.preventDefault(); dropzone.classList.remove('is-dragover'); preview(event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files[0] : null); }); overlay.addEventListener('click', function (event) { var modeButton = event.target.closest('[data-import-field-set-mode]'); if (modeButton && !modeButton.disabled) { importMode = modeButton.getAttribute('data-import-field-set-mode') === 'linked' ? 'linked' : 'copy'; overlay.querySelectorAll('[data-import-field-set-mode]').forEach(function (button) { button.setAttribute('aria-pressed', button === modeButton ? 'true' : 'false'); }); applyButton.disabled = !fingerprint || importHasConflicts; return; } if (event.target === overlay || event.target.closest('[data-field-set-import-close]')) { close(); return; } if (!event.target.closest('[data-field-set-import-apply]') || applyButton.disabled) { return; } applyButton.disabled = true; applyButton.classList.add('is-loading'); var data = new FormData(); data.append('_csrf', self.form.elements._csrf.value); data.append('descriptor', descriptor); data.append('fingerprint', fingerprint); data.append('mode', importMode); fetch(self.base() + '/rubrics/' + self.currentRubricId + '/field-sets/import/apply', { method: 'POST', body: data, headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (response) { return response.json(); }).then(function (json) { if (!json.success) { throw new Error(json.message || 'Не удалось импортировать набор'); } Adminx.Toast.show(json.message || 'Набор полей импортирован', 'success'); close(); self.loadFields(); self.applyFilterUrl(window.location.href, false); }).catch(function (error) { showError(error.message || 'Не удалось импортировать набор'); }).then(function () { applyButton.classList.remove('is-loading'); }); }); }, previewLinkedFieldSet: function (code) { if (!code || !this.currentRubricId) { return; } var self = this; fetch(this.base() + '/rubrics/' + this.currentRubricId + '/field-set-links/' + encodeURIComponent(code) + '/preview', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (response) { return response.json(); }).then(function (json) { if (!json.success) { throw new Error(json.message || 'Не удалось проверить набор'); } var data = json.data || {}; if (data.conflicts && data.conflicts.length) { Adminx.Toast.show('Нужна ручная проверка: ' + data.conflicts.join(', '), 'error'); return; } Adminx.Confirm.open({ kind: 'warning', title: 'Синхронизировать «' + (data.title || code) + '»?', message: 'Новых полей: ' + (data.created || 0) + '. Обновляемых: ' + (data.updated || 0) + '. Снятых с формы: ' + (data.detached || 0) + '. Локальные настройки будут сохранены.', confirmLabel: 'Синхронизировать', onConfirm: function () { var form = new FormData(); form.append('_csrf', self.form.elements._csrf.value); form.append('fingerprint', data.fingerprint || ''); self.ajax(self.base() + '/rubrics/' + self.currentRubricId + '/field-set-links/' + encodeURIComponent(code) + '/sync', form, function (result) { Adminx.Toast.show(result.message || 'Набор синхронизирован', 'success'); self.loadFields(); }); } }); }).catch(function (error) { Adminx.Toast.show(error.message || 'Не удалось проверить набор', 'error'); }); }, detachLinkedFieldSet: function (code) { if (!code || !this.currentRubricId) { return; } var self = this; Adminx.Confirm.open({ kind: 'danger', title: 'Удалить связь с набором?', message: 'Поля и данные документов останутся в рубрике, но больше не будут получать обновления набора.', confirmLabel: 'Удалить связь', onConfirm: function () { var form = new FormData(); form.append('_csrf', self.form.elements._csrf.value); self.ajax(self.base() + '/rubrics/' + self.currentRubricId + '/field-set-links/' + encodeURIComponent(code) + '/detach', form, function (result) { Adminx.Toast.show(result.message || 'Связь удалена', 'success'); self.loadFields(); }); } }); }, builderPaletteMarkup: function (field) { return ''; }, builderFieldMarkup: function (field, groupId) { var layout = field.layout || {}; var width = layout.width || this.builderAutoWidth(field); var conditional = field.condition && field.condition.tree && Array.isArray(field.condition.tree.items) && field.condition.tree.items.length; return '
' + '
' + '' + esc(field.type_label) + '' + (conditional ? '' : '') + '' + this.builderWidthLabel(width) + '
' + '
' + esc(field.rubric_field_title) + (layout.required ? '*' : '') + '' + esc(field.rubric_field_alias || ('field_' + field.Id)) + '
' + '
' + (layout.prefix ? '' + esc(layout.prefix) + '' : '') + '' + esc(this.builderPlaceholder(field)) + '' + (layout.suffix ? '' + esc(layout.suffix) + '' : '') + '
' + '
' + '
'; }, refreshBuilderPalette: function () { var root = document.querySelector('[data-builder-palette]'); var count = document.querySelector('[data-builder-field-count]'); if (!root) { return; } var items = root.querySelectorAll('[data-builder-palette-item]'); if (!items.length) { root.innerHTML = '
Все поля размещеныНовых полей для добавления нет.
'; } if (count) { count.textContent = items.length; } var search = document.querySelector('[data-builder-search]'); if (search && search.value) { this.filterBuilderPalette(search.value); } }, resetBuilderInspector: function () { this.builderSelectedId = 0; document.querySelectorAll('[data-builder-select].is-selected').forEach(function (node) { node.classList.remove('is-selected'); }); var empty = document.querySelector('[data-builder-inspector-empty]'); if (empty) { empty.hidden = false; } if (this.builderForm) { this.builderForm.hidden = true; } }, unplaceBuilderField: function (row) { if (!row) { return; } var id = parseInt(row.getAttribute('data-id'), 10) || 0; var field = this.builderField(id); var palette = document.querySelector('[data-builder-palette]'); if (!field || !palette) { return; } if (this.builderSelectedId === id) { this.captureBuilderInspector(); } if (!palette.querySelector('[data-builder-palette-item][data-id="' + id + '"]')) { var empty = palette.querySelector('.rubrics-builder-palette-empty'); if (empty) { empty.remove(); } palette.insertAdjacentHTML('beforeend', this.builderPaletteMarkup(field)); } field.layout = field.layout || {}; field.layout.placed = false; row.remove(); this.resetBuilderInspector(); this.cleanupBuilderEmptyStates(); this.updateBuilderGroupCounts(); this.refreshBuilderPalette(); this.setBuilderDirty(true); }, placeBuilderField: function (id, groupId, beforeNode) { id = parseInt(id, 10) || 0; groupId = parseInt(groupId, 10) || 0; var field = this.builderField(id); var target = document.querySelector('[data-builder-canvas] [data-field-group-drop="' + groupId + '"] [data-field-sortable]'); if (!field || !target) { return; } var existing = document.querySelector('[data-builder-canvas] [data-field-row][data-id="' + id + '"]'); if (existing) { this.selectBuilderField(id); return; } var empty = target.querySelector('.rubrics-builder-drop-empty'); if (empty) { empty.remove(); } target.insertAdjacentHTML('beforeend', this.builderFieldMarkup(field, groupId)); var row = target.querySelector('[data-field-row][data-id="' + id + '"]'); if (row && beforeNode && beforeNode.parentNode === target) { target.insertBefore(row, beforeNode); } var paletteItem = document.querySelector('[data-builder-palette-item][data-id="' + id + '"]'); if (paletteItem) { paletteItem.remove(); } field.layout = field.layout || {}; field.layout.placed = true; field.rubric_field_group = groupId; this.refreshBuilderPalette(); this.cleanupBuilderEmptyStates(); this.updateBuilderGroupCounts(); this.selectBuilderField(id); this.setBuilderDirty(true); }, builderField: function (id) { id = parseInt(id, 10) || 0; for (var i = 0; i < this.currentFields.length; i++) { if (parseInt(this.currentFields[i].Id, 10) === id) { return this.currentFields[i]; } } return null; }, builderTypeIcon: function (type) { type = String(type || ''); if (type.indexOf('image') !== -1 || type.indexOf('file') !== -1) { return 'ti-photo'; } if (type.indexOf('date') !== -1) { return type === 'date_time' ? 'ti-calendar-time' : 'ti-calendar'; } if (type.indexOf('checkbox') !== -1 || type === 'boolean') { return 'ti-checkbox'; } if (type.indexOf('dropdown') !== -1 || type.indexOf('select') !== -1) { return 'ti-list'; } if (type.indexOf('rub') !== -1 || type.indexOf('doc_') !== -1) { return 'ti-link'; } if (type === 'contact') { return 'ti-address-book'; } if (type === 'color') { return 'ti-palette'; } if (type === 'range') { return 'ti-arrows-horizontal'; } if (type === 'dimensions') { return 'ti-box-model-2'; } if (type === 'packages') { return 'ti-packages'; } if (type === 'address') { return 'ti-map-pin'; } if (type === 'period') { return 'ti-calendar-event'; } if (type.indexOf('numeric') !== -1 || type.indexOf('number') !== -1) { return 'ti-123'; } if (type.indexOf('text') !== -1 || type.indexOf('editor') !== -1) { return 'ti-align-left'; } return 'ti-cursor-text'; }, builderAutoWidth: function (field) { var type = String((field || {}).rubric_field_type || ''); var narrow = ['boolean', 'date', 'date_time', 'single_line', 'single_line_numeric', 'number', 'range', 'dropdown', 'drop_down', 'drop_down_key', 'doc_from_rub']; return narrow.indexOf(type) !== -1 ? 'half' : 'full'; }, builderWidthLabel: function (width) { return ({ full: '12/12', half: '6/12', third: '4/12', quarter: '3/12' })[width] || '12/12'; }, builderPlaceholder: function (field) { var type = String((field || {}).rubric_field_type || ''); if (type.indexOf('image') !== -1 || type.indexOf('file') !== -1) { return 'Выбор файла'; } if (type.indexOf('checkbox') !== -1 || type === 'boolean') { return 'Переключатель'; } if (type.indexOf('dropdown') !== -1 || type.indexOf('select') !== -1) { return 'Выбор значения'; } if (type === 'date_time') { return 'Дата и время'; } if (type === 'contact') { return 'Email, телефон или ссылка'; } if (type === 'color') { return '#rrggbb'; } if (type === 'range') { return 'От — до'; } if (type === 'dimensions') { return 'Д × Ш × В'; } if (type === 'packages') { return 'Коробки: Д × Ш × В и вес'; } if (type === 'address') { return 'Город, улица, дом'; } if (type === 'period') { return 'Начало — окончание'; } if (type.indexOf('date') !== -1) { return 'ДД.ММ.ГГГГ'; } if (type.indexOf('text') !== -1 || type.indexOf('editor') !== -1) { return 'Текстовое содержимое'; } return 'Значение поля'; }, selectBuilderField: function (id) { id = parseInt(id, 10) || 0; var field = this.builderField(id); var form = this.builderForm; if (!field || !form) { return; } if (this.builderSelectedId && this.builderSelectedId !== id) { this.captureBuilderInspector(false); } this.builderSelectedId = id; document.querySelectorAll('[data-builder-select]').forEach(function (node) { node.classList.toggle('is-selected', parseInt(node.getAttribute('data-id'), 10) === id); }); var draft = this.builderDrafts[id] || {}; var layout = field.layout || (field.layout = {}); var row = document.querySelector('[data-builder-canvas] [data-field-row][data-id="' + id + '"]'); var width = row ? row.getAttribute('data-width') : (layout.width || this.builderAutoWidth(field)); var empty = document.querySelector('[data-builder-inspector-empty]'); if (empty) { empty.hidden = true; } form.hidden = false; this.builderHydrating = true; var typeLabel = form.querySelector('[data-builder-type-label]'); var typeCode = form.querySelector('[data-builder-type-code]'); if (typeLabel) { typeLabel.textContent = field.type_label || field.rubric_field_type || 'Неизвестный тип'; } if (typeCode) { typeCode.textContent = field.rubric_field_type || '—'; } form.elements.title.value = Object.prototype.hasOwnProperty.call(draft, 'title') ? draft.title : (field.rubric_field_title || ('Поле #' + id)); this.fillBuilderGroupSelect(row ? row.closest('[data-field-group-drop]').getAttribute('data-field-group-drop') : field.rubric_field_group); var widthInput = form.querySelector('input[name="width"][value="' + width + '"]'); if (widthInput) { widthInput.checked = true; } form.elements.prefix.value = layout.prefix || ''; form.elements.suffix.value = layout.suffix || ''; form.elements.default.value = Object.prototype.hasOwnProperty.call(draft, 'default') ? draft.default : (field.rubric_field_default || ''); form.elements.description.value = Object.prototype.hasOwnProperty.call(draft, 'description') ? draft.description : (field.rubric_field_description || ''); form.elements.search.checked = Object.prototype.hasOwnProperty.call(draft, 'search') ? !!draft.search : String(field.rubric_field_search) === '1'; form.elements.numeric.checked = Object.prototype.hasOwnProperty.call(draft, 'numeric') ? !!draft.numeric : String(field.rubric_field_numeric) === '1'; var nativeNumeric = ['number', 'date_time', 'period'].indexOf(String(field.rubric_field_type || '')) !== -1; if (nativeNumeric) { form.elements.numeric.checked = true; } form.elements.numeric.disabled = nativeNumeric; form.elements.numeric.title = nativeNumeric ? 'Этот тип всегда использует числовой индекс' : ''; this.hydrateBuilderCondition(field, Object.prototype.hasOwnProperty.call(draft, 'condition') ? draft.condition : (field.condition || {})); this.builderHydrating = false; this.loadBuilderTypeSettings(field, draft.settings || null); if (row && row.scrollIntoView) { row.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } }, fillBuilderGroupSelect: function (selected) { var select = this.builderForm && this.builderForm.querySelector('[data-builder-group]'); if (!select) { return; } var html = ''; this.currentGroups.forEach(function (group) { html += ''; }); select.innerHTML = html; select.value = String(parseInt(selected, 10) || 0); }, loadBuilderTypeSettings: function (field, draftSettings) { var self = this; var root = document.querySelector('[data-builder-type-settings]'); if (!root || !field) { return; } this.builderSettingsReady = false; this.toggleBuilderDefaultField({}); root.innerHTML = '
Загрузка настроек типа...
'; fetch(this.base() + '/rubrics/fields/' + field.Id + '/plugin', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { if (self.builderSelectedId !== parseInt(field.Id, 10)) { return; } var data = json.data || {}; var type = data.type || {}; root.innerHTML = type.settings_html ? '
Настройки типа
' + type.settings_html : '

У этого типа нет дополнительных настроек.

'; if (draftSettings !== null) { self.applyBuilderSettingsDraft(root, draftSettings); } self.hydrateSettingsMaps(root); self.toggleBuilderDefaultField(type.admin_editor || {}); self.builderSettingsReady = true; }) .catch(function () { root.innerHTML = '

Не удалось загрузить настройки типа.

'; }); }, applyBuilderSettingsDraft: function (root, settings) { Object.keys(settings || {}).forEach(function (key) { var controls = root.querySelectorAll('[name="rubric_field_settings[' + key.replace(/"/g, '\\"') + ']"]'); controls.forEach(function (control) { if (control.type === 'checkbox') { control.checked = !!settings[key] && settings[key] !== '0'; } else if (Array.isArray(settings[key])) { control.value = settings[key].map(function (item) { return typeof item === 'object' ? ((item.value || '') + '=' + (item.label || '')) : item; }).join('\n'); } else { control.value = settings[key] == null ? '' : settings[key]; } }); }); }, captureBuilderInspector: function (markDirty) { if (this.builderHydrating || !this.builderSelectedId || !this.builderForm || this.builderForm.hidden) { return; } var form = this.builderForm; var field = this.builderField(this.builderSelectedId); if (!field) { return; } var settings = {}; form.querySelectorAll('[name^="rubric_field_settings["]').forEach(function (control) { var match = control.name.match(/^rubric_field_settings\[([^\]]+)\]$/); if (!match) { return; } if (control.type === 'checkbox') { if (control.checked) { settings[match[1]] = control.value || '1'; } else if (!Object.prototype.hasOwnProperty.call(settings, match[1])) { settings[match[1]] = '0'; } } else { settings[match[1]] = control.value; } }); var layout = field.layout || (field.layout = {}); layout.width = (form.querySelector('input[name="width"]:checked') || {}).value || 'full'; layout.prefix = form.elements.prefix.value.trim(); layout.suffix = form.elements.suffix.value.trim(); var draft = { id: this.builderSelectedId, title: form.elements.title.value.trim(), default: form.elements.default.value, description: form.elements.description.value, search: form.elements.search.checked ? 1 : 0, numeric: form.elements.numeric.checked ? 1 : 0 }; draft.condition = this.captureBuilderCondition(); if (this.builderSettingsReady) { draft.settings = settings; } else if (this.builderDrafts[this.builderSelectedId] && Object.prototype.hasOwnProperty.call(this.builderDrafts[this.builderSelectedId], 'settings')) { draft.settings = this.builderDrafts[this.builderSelectedId].settings; } this.builderDrafts[this.builderSelectedId] = draft; field.condition = draft.condition; if (draft.title) { field.rubric_field_title = draft.title; } this.updateBuilderFieldCard(field); if (markDirty !== false) { this.setBuilderDirty(true); } }, builderConditionOperators: function () { return [ ['equals', 'Равно'], ['not_equals', 'Не равно'], ['contains', 'Содержит'], ['not_contains', 'Не содержит'], ['empty', 'Не заполнено'], ['not_empty', 'Заполнено'], ['greater', 'Больше'], ['greater_or_equal', 'Больше или равно'], ['less', 'Меньше'], ['less_or_equal', 'Меньше или равно'], ['in', 'Одно из'], ['not_in', 'Не входит в'] ]; }, builderConditionFieldOptions: function (selected, current) { current = parseInt(current, 10) || 0; var html = ''; this.currentFields.forEach(function (field) { if (parseInt(field.Id, 10) === parseInt(current, 10)) { return; } var alias = String(field.rubric_field_alias || '').trim().toLowerCase(); var reference = 'id:' + parseInt(field.Id, 10); var label = String(field.rubric_field_title || field.rubric_field_alias || ('Поле #' + field.Id)); html += ''; }); return html; }, builderConditionRowMarkup: function (rule, current) { rule = rule || {}; var operator = String(rule.operator || 'equals'); var options = this.builderConditionOperators().map(function (item) { return ''; }).join(''); var valueHidden = operator === 'empty' || operator === 'not_empty'; return '
' + '' + '' + '' + '' + '
'; }, builderConditionGroupMarkup: function (group, depth, isRoot, current) { group = group && typeof group === 'object' ? group : {}; depth = Math.max(1, parseInt(depth, 10) || 1); var operator = group.operator === 'or' ? 'or' : 'and'; var items = Array.isArray(group.items) ? group.items : []; var self = this; var body = items.map(function (item) { return item && Array.isArray(item.items) ? self.builderConditionGroupMarkup(item, depth + 1, false, current) : self.builderConditionRowMarkup(item || {}, current); }).join(''); var title = isRoot ? 'Основная группа' : 'Вложенная группа'; var hint = operator === 'or' ? 'Достаточно любого правила внутри' : 'Должны совпасть все правила внутри'; var tools = ''; if (depth < 4) { tools += ''; } if (!isRoot) { tools += ''; } return '
' + '
' + title + '' + hint + '
' + '' + '
' + tools + '
' + '
' + body + '
'; }, hydrateBuilderConditionRoot: function (root, condition, current) { if (!root) { return; } condition = condition && typeof condition === 'object' ? condition : {}; var tree = condition.tree && typeof condition.tree === 'object' ? condition.tree : {}; var items = Array.isArray(tree.items) ? tree.items : []; root._conditionOriginal = condition; root.querySelector('[data-builder-condition-enabled]').checked = !!items.length; root.querySelector('[data-builder-condition-mode]').value = ['show', 'hide', 'lock'].indexOf(condition.mode) !== -1 ? condition.mode : 'show'; var required = root.querySelector('[data-builder-condition-required]'); if (required) { required.checked = !!condition.required; } var rows = root.querySelector('[data-builder-condition-rows]'); if (rows) { rows.innerHTML = this.builderConditionGroupMarkup({ operator: tree.operator, items: items }, 1, true, current); } this.syncBuilderCondition(root); }, hydrateBuilderCondition: function (field, condition) { var root = this.builderForm ? this.builderForm.querySelector('[data-builder-condition]') : null; this.hydrateBuilderConditionRoot(root, condition, this.builderSelectedId); this.hydrateBuilderConditionOptions(root, field, condition); this.hydrateBuilderConditionAction(root, field, condition); }, hydrateBuilderConditionOptions: function (root, field, condition) { if (!root) { return; } var section = root.querySelector('[data-builder-condition-values]'); var list = root.querySelector('[data-builder-condition-options]'); var toggle = root.querySelector('[data-builder-condition-options-enabled]'); if (!section || !list || !toggle) { return; } var options = field && Array.isArray(field.condition_options) ? field.condition_options : []; var allowed = condition && Array.isArray(condition.allowed_values) ? condition.allowed_values.map(String) : []; section.hidden = options.length === 0; toggle.checked = allowed.length > 0; list.hidden = !toggle.checked; list.innerHTML = options.map(function (option) { var value = String(option.value == null ? '' : option.value); var label = String(option.label == null ? value : option.label); return ''; }).join(''); }, hydrateBuilderConditionAction: function (root, field, condition) { if (!root) { return; } var section = root.querySelector('[data-builder-condition-action]'); var action = root.querySelector('[data-builder-condition-value-action]'); var row = root.querySelector('[data-builder-condition-action-value]'); var select = root.querySelector('[data-builder-condition-action-select]'); var input = root.querySelector('[data-builder-condition-action-input]'); if (!section || !action || !row || !select || !input) { return; } var supported = !!(field && field.condition_value_action); var options = field && Array.isArray(field.condition_action_options) ? field.condition_action_options : []; var value = condition && Object.prototype.hasOwnProperty.call(condition, 'action_value') ? String(condition.action_value) : ''; var selectedAction = condition && ['set', 'clear'].indexOf(String(condition.value_action || '')) !== -1 ? String(condition.value_action) : ''; section.hidden = !supported; action.value = supported ? selectedAction : ''; select.innerHTML = options.map(function (option) { var optionValue = String(option.value == null ? '' : option.value); var optionLabel = String(option.label == null ? optionValue : option.label); return ''; }).join(''); select.hidden = options.length === 0; input.hidden = options.length > 0; if (options.length) { select.value = value; } else { input.value = value; } row.hidden = !supported || selectedAction !== 'set'; }, syncBuilderConditionOptionSource: function (raw) { var field = this.builderField(this.builderSelectedId); var root = this.builderForm ? this.builderForm.querySelector('[data-builder-condition]') : null; if (!field || !root) { return; } var current = this.captureBuilderConditionRoot(root); var source = String(raw || '').replace(/\r\n?/g, '\n').split('\n').map(function (line) { return line.trim(); }).filter(Boolean); field.condition_options = source.map(function (line) { var match = line.match(/^([^=:|]+)\s*(?:=>|=|:|\|)\s*(.+)$/); return match ? { value: match[1].trim(), label: match[2].trim() } : { value: line, label: line }; }); field.condition_action_options = field.condition_options; this.hydrateBuilderConditionOptions(root, field, current); this.hydrateBuilderConditionAction(root, field, current); this.syncBuilderCondition(root); }, syncBuilderCondition: function (root) { root = root || (this.builderForm ? this.builderForm.querySelector('[data-builder-condition]') : null); if (!root) { return; } var enabled = root.querySelector('[data-builder-condition-enabled]').checked; var body = root.querySelector('[data-builder-condition-body]'); if (body) { body.hidden = !enabled; } var optionToggle = root.querySelector('[data-builder-condition-options-enabled]'); var optionList = root.querySelector('[data-builder-condition-options]'); if (optionToggle && optionList) { optionList.hidden = !optionToggle.checked; if (optionToggle.checked && !optionList.querySelector('[data-builder-condition-option]:checked')) { optionList.querySelectorAll('[data-builder-condition-option]').forEach(function (control) { control.checked = true; }); } } var action = root.querySelector('[data-builder-condition-value-action]'); var actionValue = root.querySelector('[data-builder-condition-action-value]'); if (action && actionValue) { actionValue.hidden = action.value !== 'set'; } root.querySelectorAll('[data-builder-condition-row]').forEach(function (row) { var operator = row.querySelector('[data-builder-condition-operator]'); var value = row.querySelector('[data-builder-condition-value]'); if (value) { value.hidden = operator && (operator.value === 'empty' || operator.value === 'not_empty'); } }); root.querySelectorAll('[data-builder-condition-group]').forEach(function (group) { var join = group.querySelector(':scope > header [data-builder-condition-join]'); var hint = group.querySelector(':scope > header [data-builder-condition-group-hint]'); if (join && hint) { hint.textContent = join.value === 'or' ? 'Достаточно любого правила внутри' : 'Должны совпасть все правила внутри'; } }); if (enabled && !root.querySelector('[data-builder-condition-row]')) { this.addBuilderConditionRow(root.querySelector('[data-builder-condition-group]'), false); } }, captureBuilderConditionGroup: function (group) { if (!group) { return { operator: 'and', items: [] }; } var join = group.querySelector(':scope > header [data-builder-condition-join]'); var itemsRoot = group.querySelector(':scope > [data-builder-condition-items]'); var items = []; Array.prototype.forEach.call(itemsRoot ? itemsRoot.children : [], function (item) { if (item.matches('[data-builder-condition-group]')) { items.push(Adminx.Rubrics.captureBuilderConditionGroup(item)); return; } if (!item.matches('[data-builder-condition-row]')) { return; } var field = item.querySelector('[data-builder-condition-field]'); var operator = item.querySelector('[data-builder-condition-operator]'); var value = item.querySelector('[data-builder-condition-value]'); if (!field || !field.value) { return; } items.push({ field: field.value, operator: operator ? operator.value : 'equals', value: value && !value.hidden ? value.value : '' }); }); return { operator: join && join.value === 'or' ? 'or' : 'and', items: items }; }, captureBuilderConditionRoot: function (root) { if (!root || !root.querySelector('[data-builder-condition-enabled]').checked) { return {}; } var required = root.querySelector('[data-builder-condition-required]'); var result = { enabled: true, mode: ['show', 'hide', 'lock'].indexOf(root.querySelector('[data-builder-condition-mode]').value) !== -1 ? root.querySelector('[data-builder-condition-mode]').value : 'show', required: required ? required.checked : false, tree: this.captureBuilderConditionGroup(root.querySelector('[data-builder-condition-group]')) }; var optionToggle = root.querySelector('[data-builder-condition-options-enabled]'); if (optionToggle && optionToggle.checked) { result.allowed_values = Array.prototype.map.call( root.querySelectorAll('[data-builder-condition-option]:checked'), function (control) { return control.value; } ); } var action = root.querySelector('[data-builder-condition-value-action]'); if (action && ['set', 'clear'].indexOf(action.value) !== -1) { result.value_action = action.value; if (action.value === 'set') { var actionSelect = root.querySelector('[data-builder-condition-action-select]'); var actionInput = root.querySelector('[data-builder-condition-action-input]'); result.action_value = actionSelect && !actionSelect.hidden ? actionSelect.value : (actionInput ? actionInput.value : ''); } } return result; }, captureBuilderCondition: function () { return this.captureBuilderConditionRoot(this.builderForm ? this.builderForm.querySelector('[data-builder-condition]') : null); }, addBuilderConditionRow: function (group, markDirty) { var root = group ? group.closest('[data-builder-condition]') : null; root = root || (this.builderForm ? this.builderForm.querySelector('[data-builder-condition]') : null); group = group || (root ? root.querySelector('[data-builder-condition-group]') : null); var rows = group ? group.querySelector(':scope > [data-builder-condition-items]') : null; if (!rows) { return; } if (root && root.querySelectorAll('[data-builder-condition-row]').length >= 32) { Adminx.Toast.show('В одном условии допускается не более 32 правил', 'warning'); return; } var current = root && root.getAttribute('data-condition-scope') === 'group' ? 0 : this.builderSelectedId; rows.insertAdjacentHTML('beforeend', this.builderConditionRowMarkup({}, current)); if (markDirty !== false && root && root.getAttribute('data-condition-scope') !== 'group') { this.captureBuilderInspector(); } var added = rows.querySelectorAll(':scope > [data-builder-condition-row]'); var select = added.length ? added[added.length - 1].querySelector('[data-builder-condition-field]') : null; if (select) { select.focus(); } }, addBuilderConditionGroup: function (group) { var items = group ? group.querySelector(':scope > [data-builder-condition-items]') : null; var depth = group ? parseInt(group.getAttribute('data-condition-depth'), 10) || 1 : 1; if (!items || depth >= 4) { return; } var root = group ? group.closest('[data-builder-condition]') : null; root = root || (this.builderForm ? this.builderForm.querySelector('[data-builder-condition]') : null); if (root && root.querySelectorAll('[data-builder-condition-row]').length >= 32) { Adminx.Toast.show('В одном условии допускается не более 32 правил', 'warning'); return; } var current = root && root.getAttribute('data-condition-scope') === 'group' ? 0 : this.builderSelectedId; items.insertAdjacentHTML('beforeend', this.builderConditionGroupMarkup({ operator: 'and', items: [{}] }, depth + 1, false, current)); if (!root || root.getAttribute('data-condition-scope') !== 'group') { this.captureBuilderInspector(); } var groups = items.querySelectorAll(':scope > [data-builder-condition-group]'); var select = groups.length ? groups[groups.length - 1].querySelector('[data-builder-condition-field]') : null; if (select) { select.focus(); } }, removeBuilderConditionRow: function (row) { var root = row ? row.closest('[data-builder-condition]') : null; if (row && row.parentNode) { row.parentNode.removeChild(row); } if (!root || root.getAttribute('data-condition-scope') !== 'group') { this.captureBuilderInspector(); } }, removeBuilderConditionGroup: function (group) { if (!group || group.classList.contains('is-root')) { return; } var root = group.closest('[data-builder-condition]'); group.remove(); if (!root || root.getAttribute('data-condition-scope') !== 'group') { this.captureBuilderInspector(); } }, toggleBuilderDefaultField: function (editor) { var field = this.builderForm ? this.builderForm.querySelector('[data-builder-default-field]') : null; if (field) { field.hidden = !!editor.default_is_configuration; } }, settingsMapRows: function (map) { if (!map) { return []; } return Array.prototype.map.call(map.querySelectorAll('[data-settings-map-row]'), function (row) { var key = row.querySelector('[data-settings-map-key]'); var label = row.querySelector('[data-settings-map-label]'); return { key: key ? key.value.trim() : '', label: label ? label.value.trim() : '' }; }); }, settingsMapRowMarkup: function (key, label) { return '
' + '' + '' + '' + '
'; }, syncSettingsMap: function (map) { var storage = map ? map.querySelector('[data-settings-map-storage]') : null; if (!storage) { return; } storage.value = this.settingsMapRows(map).filter(function (row) { return row.key !== '' || row.label !== ''; }).map(function (row) { return row.key + '=' + row.label; }).join('\n'); }, hydrateSettingsMaps: function (root) { var self = this; (root || document).querySelectorAll('[data-settings-map]').forEach(function (map) { var storage = map.querySelector('[data-settings-map-storage]'); var rows = map.querySelector('[data-settings-map-rows]'); if (!storage || !rows) { return; } var values = String(storage.value || '').split(/\r?\n/).filter(function (line) { return line.trim() !== ''; }); rows.innerHTML = values.map(function (line) { var split = line.indexOf('='); var key = split === -1 ? line : line.slice(0, split); var label = split === -1 ? line : line.slice(split + 1); return self.settingsMapRowMarkup(key.trim(), label.trim().replace(/\|\s*$/, '')); }).join('') || self.settingsMapRowMarkup('', ''); }); }, addSettingsMapRow: function (map) { var rows = map ? map.querySelector('[data-settings-map-rows]') : null; if (!rows) { return; } rows.insertAdjacentHTML('beforeend', this.settingsMapRowMarkup('', '')); this.syncSettingsMap(map); var inputs = rows.querySelectorAll('[data-settings-map-key]'); if (inputs.length) { inputs[inputs.length - 1].focus(); } if (this.builderForm && this.builderForm.contains(map)) { this.captureBuilderInspector(); } }, removeSettingsMapRow: function (row) { var map = row ? row.closest('[data-settings-map]') : null; var rows = map ? map.querySelector('[data-settings-map-rows]') : null; if (!map || !rows || !row) { return; } row.remove(); if (!rows.querySelector('[data-settings-map-row]')) { rows.innerHTML = this.settingsMapRowMarkup('', ''); } this.syncSettingsMap(map); if (this.builderForm && this.builderForm.contains(map)) { this.captureBuilderInspector(); } }, applyBuilderWidth: function (width) { var row = document.querySelector('[data-builder-canvas] [data-field-row][data-id="' + this.builderSelectedId + '"]'); if (!row) { return; } ['full', 'half', 'third', 'quarter'].forEach(function (name) { row.classList.toggle('is-' + name, name === width); }); row.setAttribute('data-width', width); var label = row.querySelector('.rubrics-builder-field-size'); if (label) { label.textContent = this.builderWidthLabel(width); } this.setBuilderDirty(true); }, moveSelectedBuilderField: function (groupId) { var row = document.querySelector('[data-builder-canvas] [data-field-row][data-id="' + this.builderSelectedId + '"]'); var target = document.querySelector('[data-builder-canvas] [data-field-group-drop="' + (parseInt(groupId, 10) || 0) + '"] [data-field-sortable]'); if (!row || !target || row.parentNode === target) { return; } target.appendChild(row); row.setAttribute('data-group', String(parseInt(groupId, 10) || 0)); this.cleanupBuilderEmptyStates(); this.updateBuilderGroupCounts(); this.setBuilderDirty(true); }, updateBuilderFieldCard: function (field) { var row = document.querySelector('[data-builder-canvas] [data-field-row][data-id="' + field.Id + '"]'); if (!row) { return; } var layout = field.layout || {}; var conditional = field.condition && field.condition.tree && Array.isArray(field.condition.tree.items) && field.condition.tree.items.length; var top = row.querySelector('.rubrics-builder-field-top'); var marker = row.querySelector('.rubrics-builder-field-condition'); if (conditional && !marker && top) { var size = top.querySelector('.rubrics-builder-field-size'); var html = ''; if (size) { size.insertAdjacentHTML('beforebegin', html); } else { top.insertAdjacentHTML('beforeend', html); } } else if (!conditional && marker) { marker.remove(); } var title = row.querySelector('.rubrics-builder-field-main > b'); if (title) { title.innerHTML = esc(field.rubric_field_title || ('Поле #' + field.Id)) + (layout.required ? '*' : ''); } var preview = row.querySelector('.rubrics-builder-field-preview'); if (preview) { preview.innerHTML = (layout.prefix ? '' + esc(layout.prefix) + '' : '') + '' + esc(this.builderPlaceholder(field)) + '' + (layout.suffix ? '' + esc(layout.suffix) + '' : ''); } }, filterBuilderPalette: function (query) { query = String(query || '').trim().toLowerCase(); document.querySelectorAll('[data-builder-palette-item]').forEach(function (item) { item.hidden = query !== '' && String(item.getAttribute('data-builder-search-text') || '').toLowerCase().indexOf(query) === -1; }); }, updateBuilderGroupCounts: function () { document.querySelectorAll('[data-builder-canvas] [data-field-group-drop]').forEach(function (group) { var count = group.querySelector('[data-builder-group-count]'); if (count) { count.textContent = group.querySelectorAll('[data-field-row]').length; } }); }, cleanupBuilderEmptyStates: function () { document.querySelectorAll('[data-builder-canvas] [data-field-sortable]').forEach(function (list) { var empty = list.querySelector('.rubrics-builder-drop-empty'); var hasFields = !!list.querySelector('[data-field-row]'); if (empty && hasFields) { empty.remove(); } if (!empty && !hasFields) { list.innerHTML = '
Перетащите поле в эту группу
'; } }); }, setBuilderDirty: function (dirty) { this.builderDirty = !!dirty; var button = document.querySelector('[data-builder-save]'); var state = document.querySelector('[data-builder-save-state]'); if (button) { button.disabled = !this.builderDirty; } if (state) { state.hidden = !this.builderDirty; } }, syncBuilderRuntime: function () { var root = document.querySelector('[data-builder-runtime]'); var toggle = document.querySelector('[data-builder-conditions-enabled]'); var state = document.querySelector('[data-builder-runtime-state]'); if (!root || !toggle || !state) { return; } var enabled = !!toggle.checked; root.classList.toggle('is-enabled', enabled); state.className = 'badge ' + (enabled ? 'badge-green' : 'badge-gray'); state.textContent = enabled ? 'Включены' : 'Выключены'; }, openBuilderAdvanced: function () { var row = document.querySelector('[data-builder-canvas] [data-field-row][data-id="' + this.builderSelectedId + '"]'); if (!row) { return; } if (this.builderDirty) { Adminx.Toast.show('Сначала сохраните изменения конструктора', 'warning'); return; } this.fillFieldEdit(row); if (Adminx.Drawer) { Adminx.Drawer.open('rubricFieldDrawer'); } }, saveBuilder: function () { var self = this; if (!this.builderDirty) { return; } this.captureBuilderInspector(); var payload = this.builderSavePayload(); var previewData = this.builderFormData(payload, ''); this.ajax(this.base() + '/rubrics/' + this.currentRubricId + '/fields/builder/preview', previewData, function (json) { var impact = (json.data || {}).impact || {}; var persist = function () { self.persistBuilder(payload, impact.fingerprint || ''); }; if (impact.requires_confirmation) { self.showBuilderImpact(impact, persist); return; } persist(); }); }, builderSavePayload: function () { var self = this; var layout = []; document.querySelectorAll('[data-builder-canvas] [data-field-group-drop]').forEach(function (group) { var groupId = parseInt(group.getAttribute('data-field-group-drop'), 10) || 0; group.querySelectorAll('[data-field-row]').forEach(function (row) { var field = self.builderField(row.getAttribute('data-id')); var fieldLayout = field && field.layout ? field.layout : {}; layout.push({ id: parseInt(row.getAttribute('data-id'), 10), group_id: groupId, width: row.getAttribute('data-width') || 'full', prefix: fieldLayout.prefix || '', suffix: fieldLayout.suffix || '' }); }); }); var drafts = Object.keys(this.builderDrafts).map(function (id) { return self.builderDrafts[id]; }); return { layout: layout, drafts: drafts, conditionsEnabled: !!(document.querySelector('[data-builder-conditions-enabled]') || {}).checked }; }, builderFormData: function (payload, fingerprint) { var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); data.append('layout', JSON.stringify(payload.layout || [])); data.append('drafts', JSON.stringify(payload.drafts || [])); data.append('conditions_enabled', payload.conditionsEnabled ? '1' : '0'); if (fingerprint) { data.append('impact_fingerprint', fingerprint); } return data; }, persistBuilder: function (payload, fingerprint) { var self = this; var data = this.builderFormData(payload, fingerprint); this.ajax(this.base() + '/rubrics/' + this.currentRubricId + '/fields/builder', data, function () { self.setBuilderDirty(false); self.builderDrafts = {}; Adminx.Toast.show('Раскладка полей сохранена', 'success'); self.loadFields(); }); }, showBuilderImpact: function (impact, onConfirm) { var overlay = document.createElement('div'); var enabled = !!impact.enabled_after; var total = parseInt(impact.documents_total, 10) || 0; var analyzed = parseInt(impact.documents_analyzed, 10) || 0; var details = Array.isArray(impact.details) ? impact.details : []; var detailsHtml = details.map(function (item) { return '
' + '' + esc(item.title || ('Поле #' + item.id)) + '' + esc(item.alias || ('field_' + item.id)) + '' + '' + (parseInt(item.changed_documents, 10) || 0) + 'изменится' + '' + (parseInt(item.hidden_before, 10) || 0) + ' → ' + (parseInt(item.hidden_after, 10) || 0) + 'скрыто · заблокировано ' + (parseInt(item.locked_before, 10) || 0) + ' → ' + (parseInt(item.locked_after, 10) || 0) + ' · ограничено ' + (parseInt(item.limited_before, 10) || 0) + ' → ' + (parseInt(item.limited_after, 10) || 0) + ' · автодействие ' + (parseInt(item.action_before, 10) || 0) + ' → ' + (parseInt(item.action_after, 10) || 0) + ' · обязательно ' + (parseInt(item.required_before, 10) || 0) + ' → ' + (parseInt(item.required_after, 10) || 0) + '' + '
'; }).join(''); if (!detailsHtml) { detailsHtml = '
Правила полей не изменились.
'; } overlay.className = 'overlay rubrics-builder-impact-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); document.removeEventListener('keydown', onKey); }, 180); }; var onKey = function (e) { if (e.key === 'Escape') { close(); } }; overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-impact-cancel]')) { close(); return; } if (e.target.closest('[data-impact-confirm]')) { close(); onConfirm(); } }); document.addEventListener('keydown', onKey); requestAnimationFrame(function () { overlay.classList.add('show'); var button = overlay.querySelector('[data-impact-confirm]'); if (button) { button.focus(); } }); }, renderGroupList: function (groups) { var html = ''; groups.forEach(function (group) { var condition = group.condition && typeof group.condition === 'object' ? group.condition : {}; html += '
' + '' + '
' + esc(group.title) + '' + esc(group.fields_count) + ' полей' + (condition.tree ? ' · есть условие' : '') + '
' + '
' + '' + '' + '
'; }); if (!html) { html = '
Группы не созданы.
'; } var root = document.querySelector('[data-rubric-groups-list]'); if (root) { root.innerHTML = html; } }, fillFieldNew: function () { this.clearErrors(this.fieldForm); this.fieldForm.reset(); this.fieldForm.elements.id.value = ''; this.fieldForm.elements.rubric_id.value = this.currentRubricId; this.syncFieldTypeOptions(''); document.getElementById('rubricFieldTitle').textContent = 'Новое поле'; this.fillGroupSelect(); this.setHint('[data-field-alias-state]', ''); this.syncFieldTemplateEditors(); this.syncNativeNumericType(this.fieldForm.elements.rubric_field_type.value); this.loadTypeInfo(this.fieldForm.elements.rubric_field_type.value); if (Adminx.Drawer) { Adminx.Drawer.open('rubricFieldDrawer'); } }, resetFieldForm: function () { if (!this.fieldForm) { return; } clearTimeout(this.fieldAliasTimer); this.fieldAliasTimer = null; this.activeFieldTemplateTextarea = null; this.clearErrors(this.fieldForm); this.fieldForm.reset(); this.fieldForm.elements.id.value = ''; this.fieldForm.elements.rubric_id.value = this.currentRubricId || ''; this.setHint('[data-field-alias-state]', ''); this.syncFieldTypeOptions(''); ['rubric_field_template', 'rubric_field_template_request'].forEach(function (name) { var textarea = this.fieldForm.elements[name]; if (textarea && textarea._adminxCodeMirror) { textarea._adminxCodeMirror.setValue(''); textarea._adminxCodeMirror.save(); } }, this); var plugin = document.querySelector('[data-field-plugin-body]'); if (plugin) { plugin.innerHTML = '
Выберите тип поля.
'; } var title = document.getElementById('rubricFieldTitle'); if (title) { title.textContent = 'Новое поле'; } }, /** Перенести значения textarea шаблонов поля в CodeMirror и обновить отрисовку. */ syncFieldTemplateEditors: function () { var form = this.fieldForm; if (!form) { return; } ['rubric_field_template', 'rubric_field_template_request'].forEach(function (name) { var ta = form.elements[name]; if (ta && ta._adminxCodeMirror) { ta._adminxCodeMirror.setValue(ta.value || ''); } }); this.refreshEditors(); }, /** Вставить тег из легенды в активный (или первый) редактор шаблона поля. */ insertFieldTemplateTag: function (tag) { if (!tag || !this.fieldForm) { return; } var ta = this.activeFieldTemplateTextarea; if (!ta || !ta.closest || !ta.closest('#rubricFieldForm')) { ta = this.fieldForm.elements.rubric_field_template; } if (!ta) { return; } var cm = ta._adminxCodeMirror; if (cm) { cm.replaceSelection(tag); cm.focus(); cm.save(); Adminx.Toast.show('Тег вставлен', 'success'); return; } var start = ta.selectionStart || 0; ta.value = ta.value.slice(0, start) + tag + ta.value.slice(ta.selectionEnd || start); ta.focus(); }, fillFieldEdit: function (row) { if (!row) { return; } var self = this; this.clearErrors(this.fieldForm); fetch(this.base() + '/rubrics/fields/' + row.getAttribute('data-id'), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var item = json.data || {}; self.fillGroupSelect(); self.fieldForm.elements.id.value = item.Id || ''; self.fieldForm.elements.rubric_id.value = item.rubric_id || self.currentRubricId; self.fieldForm.elements.rubric_field_title.value = item.rubric_field_title || ''; self.fieldForm.elements.rubric_field_alias.value = item.rubric_field_alias || ''; self.syncFieldTypeOptions(item.rubric_field_type || ''); self.fieldForm.elements.rubric_field_type.value = item.rubric_field_type || ''; self.fieldForm.elements.rubric_field_group.value = String(item.rubric_field_group || 0); self.fieldForm.elements.rubric_field_search.value = String(item.rubric_field_search || 0); self.fieldForm.elements.rubric_field_numeric.value = String(item.rubric_field_numeric || 0); self.syncNativeNumericType(item.rubric_field_type || ''); self.fieldForm.elements.rubric_field_default.value = item.rubric_field_default || ''; self.fieldForm.elements.rubric_field_description.value = item.rubric_field_description || ''; self.fieldForm.elements.rubric_field_template.value = item.rubric_field_template || ''; self.fieldForm.elements.rubric_field_template_request.value = item.rubric_field_template_request || ''; var L = item.layout || {}; var el = self.fieldForm.elements; if (el['rubric_field_layout[width]']) { el['rubric_field_layout[width]'].value = L.width || ''; } if (el['rubric_field_layout[prefix]']) { el['rubric_field_layout[prefix]'].value = L.prefix || ''; } if (el['rubric_field_layout[suffix]']) { el['rubric_field_layout[suffix]'].value = L.suffix || ''; } document.getElementById('rubricFieldTitle').textContent = 'Редактирование поля #' + item.Id; self.setHint('[data-field-alias-state]', ''); self.syncFieldTemplateEditors(); self.loadFieldPlugin(item.Id); }); }, loadTypeInfo: function (type) { var root = document.querySelector('[data-field-plugin-body]'); var self = this; if (!root || !type) { return; } var defaultWrap = document.querySelector('.rubrics-default-field'); if (defaultWrap) { defaultWrap.hidden = false; } root.innerHTML = '
Загрузка типа поля...
'; fetch(this.base() + '/rubrics/field-types/' + encodeURIComponent(type), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { self.renderPluginInfo({ type: json.data || null, field: null, render: null }); }) .catch(function () { root.innerHTML = '
Не удалось загрузить тип поля.
'; }); }, syncNativeNumericType: function (type) { if (!this.fieldForm || !this.fieldForm.elements.rubric_field_numeric) { return; } var numeric = this.fieldForm.elements.rubric_field_numeric; var inherent = ['number', 'date_time', 'period'].indexOf(String(type || '')) !== -1; if (inherent) { numeric.value = '1'; } numeric.disabled = inherent; numeric.title = inherent ? 'Этот тип всегда использует числовой индекс' : ''; }, loadFieldPlugin: function (id) { var root = document.querySelector('[data-field-plugin-body]'); var self = this; if (!root || !id) { return; } var defaultWrap = document.querySelector('.rubrics-default-field'); if (defaultWrap) { defaultWrap.hidden = false; } root.innerHTML = '
Загрузка настроек типа...
'; fetch(this.base() + '/rubrics/fields/' + encodeURIComponent(id) + '/plugin', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { self.renderPluginInfo(json.data || {}); }) .catch(function () { root.innerHTML = '
Не удалось загрузить настройки типа.
'; }); }, renderFieldSettings: function (html) { if (!html) { return ''; } return '
' + '
Настройки поля
' + html + '
'; }, renderPluginInfo: function (data) { var root = document.querySelector('[data-field-plugin-body]'); var type = data.type || null; var render = data.render || null; var statusClass; var statusText; if (!root) { return; } if (!type) { root.innerHTML = '
Тип поля не зарегистрирован.
'; return; } this.applyFieldAdminEditor(type.admin_editor || {}); statusClass = type.status === 'ok' ? 'badge-green' : (type.status === 'error' ? 'badge-red' : 'badge-amber'); statusText = type.status === 'ok' ? 'готов' : (type.status === 'error' ? 'ошибка' : 'проверить'); var settings = this.renderFieldSettings(type.settings_html || ''); // Показываем только реальные настройки типа. Диагностика плагина // (источник/функция/шаблоны/ассеты/preview) — не для контент-редактора; // проблемы показываем лишь если они есть. var issues = (type.issues && type.issues.length) ? this.renderPluginIssues(type.issues) : ''; root.innerHTML = '
' + '
' + esc(type.name) + '' + esc(type.id) + '
' + '' + statusText + '' + '
' + (settings || '

У этого типа нет дополнительных настроек.

') + issues; }, renderAdminEditor: function (editor) { var controls = editor.controls || []; var status = editor.status === 'native' ? 'adminx' : 'fallback'; var badge = editor.status === 'native' ? 'badge-green' : 'badge-amber'; if (!editor || !editor.title) { return ''; } return '
' + '
' + '' + '
' + esc(editor.title) + '' + esc(editor.summary || '') + '
' + '' + status + '' + '
' + '
' + controls.map(function (control) { return '' + esc(control.label || control.name) + '' + esc(Adminx.Rubrics.kindLabel(control.kind || 'text')) + ''; }).join('') + '
' + this.renderAdminEntityFields(editor.entity_fields || []) + '
'; }, renderAdminEntityFields: function (fields) { if (!fields.length) { return ''; } return '
' + 'Сущность поля' + '
' + fields.map(function (field) { return '' + esc(field) + ''; }).join('') + '
' + '
'; }, kindLabel: function (kind) { var map = { text: 'короткий текст', textarea: 'многострочный текст', lines: 'список строк', key_value: 'ключ и значение', boolean: 'да / нет', date: 'дата', datetime: 'дата и время', number: 'число', range: 'диапазон', dimensions: 'три размера', packages: 'список упаковок', address: 'структурированный адрес', period: 'период дат', pipe: 'части через |', pipe_list: 'строки с колонками', media: 'медиафайл', media_list: 'список файлов', relation: 'документ', relation_list: 'список документов', url: 'ссылка', code: 'код' }; return map[kind] || kind; }, applyFieldAdminEditor: function (editor) { var self = this; var controls = editor.controls || []; var byName = {}; controls.forEach(function (control) { if (control.name) { byName[control.name] = control; } }); var defaultWrap = document.querySelector('.rubrics-default-field'); if (defaultWrap) { defaultWrap.hidden = !!editor.default_is_configuration; } ['rubric_field_default', 'rubric_field_template', 'rubric_field_template_request'].forEach(function (name) { var control = byName[name] || {}; var label = document.querySelector('[data-field-control-label="' + name + '"]'); var hint = document.querySelector('[data-field-control-hint="' + name + '"]'); var input = self.fieldForm ? self.fieldForm.elements[name] : null; if (label && control.label) { label.textContent = control.label; } if (hint) { hint.textContent = control.hint || ''; } if (input) { input.setAttribute('data-admin-editor-kind', control.kind || 'text'); input.placeholder = self.placeholderForControl(control.kind || 'text'); if (name === 'rubric_field_default') { input.rows = self.rowsForControl(control.kind || 'text'); input.value = self.storageValueToEditor(input.value, control.kind || 'text'); self.renderDefaultEditor(control.kind || 'text'); } } }); }, renderDefaultEditor: function (kind) { var storage = this.defaultStorage(); var root = document.querySelector('[data-field-default-editor]'); var value; if (!storage || !root) { return; } value = this.storageValueToEditor(storage.value, kind); storage.value = value; storage.hidden = false; root.hidden = true; root.innerHTML = ''; storage.classList.toggle('mono', kind === 'code'); if (['text', 'url', 'media', 'date', 'datetime', 'number', 'pipe', 'relation'].indexOf(kind) !== -1) { var inputType = kind === 'date' ? 'date' : (kind === 'datetime' ? 'datetime-local' : (kind === 'url' ? 'url' : (kind === 'number' || kind === 'relation' ? 'number' : 'text'))); var inputIcon = kind === 'date' ? 'calendar' : (kind === 'datetime' ? 'calendar-time' : (kind === 'url' ? 'link' : (kind === 'media' ? 'photo' : (kind === 'relation' ? 'file-search' : (kind === 'number' ? 'number' : 'forms'))))); storage.hidden = true; root.hidden = false; root.innerHTML = '
' + '' + '' + (kind === 'media' ? '' : '') + (kind === 'relation' ? '' : '') + '
'; return; } if (kind === 'boolean') { storage.hidden = true; root.hidden = false; root.innerHTML = '
' + '' + '' + '
'; this.setDefaultBoolean(value === '1' ? '1' : '0'); return; } if (['lines', 'key_value', 'media_list', 'pipe_list', 'relation_list', 'packages'].indexOf(kind) !== -1) { storage.hidden = true; root.hidden = false; root.setAttribute('data-default-list-kind', kind); root.innerHTML = '
' + '
' + '' + '
'; this.renderDefaultListRows(this.defaultListParts(value, kind), kind); } }, defaultStorage: function () { return this.fieldForm ? this.fieldForm.querySelector('[data-field-default-storage]') : null; }, syncDefaultEditorValue: function (input) { var storage = this.defaultStorage(); if (!storage || !input) { return; } storage.value = input.value || ''; }, setDefaultBoolean: function (value) { var storage = this.defaultStorage(); value = value === '1' ? '1' : '0'; if (storage) { storage.value = value; } document.querySelectorAll('[data-default-boolean]').forEach(function (btn) { var active = btn.getAttribute('data-default-boolean') === value; btn.classList.toggle('is-active', active); btn.setAttribute('aria-pressed', active ? 'true' : 'false'); }); }, defaultListParts: function (value, kind) { value = String(value == null ? '' : value); if (kind === 'media_list') { return value.split(/[\r\n]+/).map(function (part) { return part.trim(); }).filter(Boolean); } if (kind === 'pipe_list') { return value.split(/[,\r\n]+/).map(function (part) { return part.trim(); }).filter(Boolean); } if (kind === 'relation_list') { return value.split(/[|,\r\n]+/).map(function (part) { return part.trim(); }).filter(Boolean); } if (kind === 'packages') { return value.split(/[\r\n]+/).map(function (part) { return part.trim(); }).filter(Boolean); } return value.split(/[,\r\n]+/).map(function (part) { return part.trim(); }).filter(Boolean); }, renderDefaultListRows: function (parts, kind) { var root = document.querySelector('[data-default-list-rows]'); if (!root) { return; } if (!parts.length) { parts = ['']; } root.classList.toggle('is-key-value', kind === 'key_value'); root.innerHTML = parts.map(function (part, index) { var placeholder = kind === 'key_value' ? 'key|Название' : (kind === 'media_list' ? '/uploads/file.webp' : (kind === 'relation_list' ? 'ID документа' : (kind === 'pipe_list' ? 'Колонка1|Колонка2|Колонка3' : (kind === 'packages' ? 'длина|ширина|высота|вес' : 'Вариант')))); var keyValue; if (kind === 'key_value') { keyValue = part.split('|'); return '
' + '' + (index + 1) + '' + '' + '' + '' + '
'; } return '
' + '' + (index + 1) + '' + '' + (kind === 'media_list' ? '' : '') + (kind === 'relation_list' ? '' : '') + '' + '
'; }).join(''); this.syncDefaultListValue(); }, addDefaultListRow: function () { var root = document.querySelector('[data-default-list-rows]'); var kindRoot = document.querySelector('[data-default-list-kind]'); var kind = kindRoot ? kindRoot.getAttribute('data-default-list-kind') : 'lines'; var parts = this.readDefaultListInputs(); parts.push(''); this.renderDefaultListRows(parts, kind); var inputs = root ? root.querySelectorAll('[data-default-list-input], [data-default-list-key]') : []; if (inputs.length) { inputs[inputs.length - 1].focus(); } }, removeDefaultListRow: function (row) { var kindRoot = document.querySelector('[data-default-list-kind]'); var kind = kindRoot ? kindRoot.getAttribute('data-default-list-kind') : 'lines'; var parts; if (row) { row.remove(); } parts = this.readDefaultListInputs(); this.renderDefaultListRows(parts.length ? parts : [''], kind); }, readDefaultListInputs: function () { var keyRows = document.querySelectorAll('[data-default-list-row].is-key-value'); if (keyRows.length) { return Array.prototype.map.call(keyRows, function (row) { var key = row.querySelector('[data-default-list-key]'); var value = row.querySelector('[data-default-list-value]'); key = key ? key.value.trim() : ''; value = value ? value.value.trim() : ''; return key || value ? key + '|' + value : ''; }); } return Array.prototype.map.call(document.querySelectorAll('[data-default-list-input]'), function (input) { return input.value || ''; }); }, syncDefaultListValue: function () { var storage = this.defaultStorage(); var kindRoot = document.querySelector('[data-default-list-kind]'); var kind = kindRoot ? kindRoot.getAttribute('data-default-list-kind') : 'lines'; var separator = kind === 'media_list' ? '\n' : '\n'; if (!storage) { return; } storage.value = this.readDefaultListInputs().map(function (part) { return String(part || '').trim(); }).filter(Boolean).join(separator); }, openDefaultMediaPicker: function (row) { var self = this; Adminx.MediaPicker.open({ type: 'image', title: 'Выбрать файл', description: 'Файл будет записан в значение по умолчанию поля.', onPick: function (file) { if (file && file.url) { self.applyPickedMedia(file.url, row); } } }); }, applyPickedMedia: function (url, row) { var input; if (row) { input = row.querySelector('[data-default-list-input]'); if (input) { input.value = url; this.syncDefaultListValue(); } return; } input = document.querySelector('[data-default-editor-input]'); if (input) { input.value = url; this.syncDefaultEditorValue(input); } }, openDefaultRelationPicker: function (row) { var self = this; var overlay = document.createElement('div'); overlay.className = 'overlay rubrics-relation-picker-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); this.bindDefaultRelationPicker(overlay, row); }, bindDefaultRelationPicker: function (overlay, targetRow) { var self = this; var list = overlay.querySelector('[data-relation-list]'); var status = overlay.querySelector('[data-relation-status]'); var search = overlay.querySelector('[data-relation-search]'); var count = overlay.querySelector('[data-relation-count]'); var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); document.removeEventListener('keydown', onKey); }, 180); }; var render = function (items) { list.innerHTML = ''; (items || []).forEach(function (item) { list.insertAdjacentHTML('beforeend', ''); }); status.hidden = items && items.length > 0; status.textContent = search.value.trim() ? 'Ничего не найдено' : 'Начните вводить название или ID'; count.textContent = items && items.length ? 'Документов: ' + items.length : ''; }; var load = function () { status.textContent = 'Загрузка...'; status.hidden = false; var params = new URLSearchParams(); params.set('q', search.value.trim()); params.set('limit', 20); fetch(self.base() + '/rubrics/documents/picker?' + params.toString(), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var data = json.data && json.data.success ? json.data.data : (json.data || {}); render(data.items || []); }) .catch(function () { list.innerHTML = ''; status.textContent = 'Не удалось загрузить документы'; status.hidden = false; }); }; var apply = function (id) { self.applyPickedRelation(id, targetRow); close(); }; 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-relation-cancel]')) { close(); return; } var item = e.target.closest('[data-relation-id]'); if (item) { apply(item.getAttribute('data-relation-id')); } }); search.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(load, 220); }); document.addEventListener('keydown', onKey); load(); search.focus(); }, applyPickedRelation: function (id, row) { var input; if (row) { input = row.querySelector('[data-default-list-input]'); if (input) { input.value = id; this.syncDefaultListValue(); } return; } input = document.querySelector('[data-default-editor-input]'); if (input) { input.value = id; this.syncDefaultEditorValue(input); } }, storageValueToEditor: function (value, kind) { value = String(value == null ? '' : value); if (kind === 'packages' && value.trim().charAt(0) === '[') { try { var packages = JSON.parse(value); if (Array.isArray(packages)) { return packages.map(function (item) { item = item || {}; return [item.length || '', item.width || '', item.height || '', item.weight || ''].join('|'); }).join('\n'); } } catch (ignore) {} } if (kind === 'address' && value.trim().charAt(0) === '{') { try { var address = JSON.parse(value) || {}; return ['postal_code', 'region', 'city', 'street', 'building', 'unit', 'latitude', 'longitude'].map(function (key) { return address[key] || ''; }).join('|'); } catch (ignoreAddress) {} } if (kind === 'period' && value.trim().charAt(0) === '{') { try { var period = JSON.parse(value) || {}; var periodPart = function (timestamp) { if (!/^\d+$/.test(String(timestamp || ''))) { return ''; } var date = new Date(parseInt(timestamp, 10) * 1000); var month = String(date.getMonth() + 1).padStart(2, '0'); var day = String(date.getDate()).padStart(2, '0'); var hours = String(date.getHours()).padStart(2, '0'); var minutes = String(date.getMinutes()).padStart(2, '0'); return date.getFullYear() + '-' + month + '-' + day + ' ' + hours + ':' + minutes; }; return periodPart(period.start) + '|' + periodPart(period.end); } catch (ignorePeriod) {} } if ((kind === 'lines' || kind === 'key_value') && value.indexOf('\n') === -1) { if (kind === 'lines' && value.indexOf('|') !== -1 && value.indexOf(',') === -1) { return value.split('|').map(function (part) { return part.trim(); }).filter(Boolean).join('\n'); } return value.split(',').map(function (part) { return part.trim(); }).filter(Boolean).join('\n'); } if (kind === 'pipe_list' && value.indexOf('\n') === -1) { return value.split(',').map(function (part) { return part.trim(); }).filter(Boolean).join('\n'); } if (kind === 'relation_list' && value.indexOf('\n') === -1) { return value.split(/[|,]+/).map(function (part) { return part.trim(); }).filter(Boolean).join('\n'); } if (kind === 'boolean') { value = value.trim().toLowerCase(); return ['1', 'true', 'yes', 'on', 'да', 'вкл', 'включено'].indexOf(value) !== -1 ? '1' : '0'; } if (kind === 'date' && /^\d{9,10}$/.test(value)) { var date = new Date(parseInt(value, 10) * 1000); var month = String(date.getMonth() + 1).padStart(2, '0'); var day = String(date.getDate()).padStart(2, '0'); return date.getFullYear() + '-' + month + '-' + day; } if (kind === 'datetime' && /^\d{9,10}$/.test(value)) { var dateTime = new Date(parseInt(value, 10) * 1000); var dateTimeMonth = String(dateTime.getMonth() + 1).padStart(2, '0'); var dateTimeDay = String(dateTime.getDate()).padStart(2, '0'); var dateTimeHours = String(dateTime.getHours()).padStart(2, '0'); var dateTimeMinutes = String(dateTime.getMinutes()).padStart(2, '0'); return dateTime.getFullYear() + '-' + dateTimeMonth + '-' + dateTimeDay + 'T' + dateTimeHours + ':' + dateTimeMinutes; } return value; }, editorValueToStorage: function (value, kind) { value = String(value == null ? '' : value); if (kind === 'lines' || kind === 'key_value' || kind === 'pipe_list' || kind === 'relation_list') { return value.split(/[,\r\n]+/).map(function (part) { return part.trim(); }).filter(Boolean).join(','); } if (kind === 'boolean') { value = value.trim().toLowerCase(); return ['1', 'true', 'yes', 'on', 'да', 'вкл', 'включено'].indexOf(value) !== -1 ? '1' : '0'; } return value; }, placeholderForControl: function (kind) { var map = { text: 'Текстовое значение', textarea: 'Текст или параметры поля', lines: 'Один вариант на строку', key_value: 'key|Название', boolean: '0 или 1', date: 'YYYY-MM-DD', datetime: 'YYYY-MM-DD HH:MM', number: '0', range: 'минимум|максимум', dimensions: 'длина|ширина|высота', packages: 'длина|ширина|высота|вес', address: 'индекс|регион|город|улица|дом|помещение|широта|долгота', period: 'YYYY-MM-DD|YYYY-MM-DD', pipe: 'значение1|значение2', pipe_list: 'Колонка1|Колонка2|Колонка3', relation: 'ID документа', relation_list: 'ID документа', media: '/uploads/example.webp', media_list: '/uploads/one.webp\n/uploads/two.webp', url: '/catalog', code: '' }; return map[kind] || ''; }, rowsForControl: function (kind) { if (kind === 'text' || kind === 'boolean' || kind === 'date' || kind === 'datetime' || kind === 'url' || kind === 'media' || kind === 'number' || kind === 'range' || kind === 'dimensions' || kind === 'period' || kind === 'pipe' || kind === 'relation') { return 2; } if (kind === 'address') { return 3; } if (kind === 'lines' || kind === 'key_value' || kind === 'media_list' || kind === 'pipe_list' || kind === 'relation_list' || kind === 'packages') { return 7; } if (kind === 'code') { return 8; } return 4; }, renderPluginIssues: function (issues) { if (!issues.length) { return '
Критичных проблем не найдено.
'; } return '
' + issues.map(function (issue) { return '' + esc(issue) + ''; }).join('') + '
'; }, renderPluginFiles: function (title, files) { if (!files.length) { return ''; } return '
' + esc(title) + '' + files.slice(0, 6).map(function (file) { return '' + esc(file) + ''; }).join('') + (files.length > 6 ? '+' + (files.length - 6) + '' : '') + '
'; }, renderPluginPreview: function (render) { var statusClass; if (!render) { return ''; } statusClass = render.status === 'ok' ? 'is-ok' : (render.status === 'error' ? 'is-error' : 'is-warning'); return '
' + '
' + 'Preview edit-режима' + '' + esc(render.status || 'info') + '' + '
' + '

' + esc(render.message || '') + '

' + this.renderPluginNotes(render.notes || []) + (render.html ? '
' + render.html + '
' : '') + this.renderPluginFiles('Preview assets', render.assets || []) + '
'; }, renderPluginNotes: function (notes) { if (!notes.length) { return ''; } return '
' + notes.map(function (note) { return '' + esc(note) + ''; }).join('') + '
'; }, submitField: function () { var self = this; var id = this.fieldForm.elements.id.value; var rubricId = this.fieldForm.elements.rubric_id.value || this.currentRubricId; if (window.Adminx && Adminx.CodeEditor && Adminx.CodeEditor.syncAll) { Adminx.CodeEditor.syncAll(this.fieldForm); } this.clearErrors(this.fieldForm); this.checkFieldAlias(true, function (ok) { if (!ok) { return; } self.ajax(self.base() + (id ? '/rubrics/fields/' + id : '/rubrics/' + rubricId + '/fields'), self.fieldFormData(), function () { Adminx.Toast.show('Поле сохранено', 'success'); if (Adminx.Drawer) { Adminx.Drawer.close('rubricFieldDrawer'); } self.loadFields(); self.applyFilterUrl(window.location.href, false); }, function (json) { self.showErrors(self.fieldForm, json); }); }); }, fieldFormData: function () { var data = new FormData(this.fieldForm); var input = this.fieldForm.elements.rubric_field_default; var kind = input ? input.getAttribute('data-admin-editor-kind') : ''; if (input) { data.set('rubric_field_default', this.editorValueToStorage(input.value, kind)); } return data; }, checkFieldAlias: function (required, done) { var alias = this.fieldForm.elements.rubric_field_alias.value.trim(); var id = this.fieldForm.elements.id.value || 0; var rubricId = this.fieldForm.elements.rubric_id.value || this.currentRubricId; if (!alias && !required) { this.setHint('[data-field-alias-state]', ''); if (done) { done(false); } return; } var self = this; fetch(this.base() + '/rubrics/fields/alias-check?alias=' + encodeURIComponent(alias) + '&rubric_id=' + encodeURIComponent(rubricId) + '&id=' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (json) { var data = json.data || {}; self.setHint('[data-field-alias-state]', data.message || '', data.valid && data.available); if (done) { done(!!(data.valid && data.available)); } }); }, deleteField: function (row) { if (!row || !window.confirm('Удалить поле? Значения документов для этого поля тоже будут удалены.')) { return; } var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); var self = this; this.ajax(this.base() + '/rubrics/fields/' + row.getAttribute('data-id') + '/delete', data, function () { Adminx.Toast.show('Поле удалено', 'success'); self.loadFields(); self.applyFilterUrl(window.location.href, false); }); }, openSchemaRevisions: function (row) { if (!row) { return; } this.currentSchemaRevisionRubricId = parseInt(row.getAttribute('data-id'), 10) || 0; this.currentSchemaRevisionId = 0; this.currentSchemaRevisionFingerprint = ''; var title = document.getElementById('rubricSchemaRevisionsTitle'); var subtitle = document.querySelector('[data-schema-revisions-subtitle]'); if (title) { title.textContent = 'Ревизии: ' + (row.getAttribute('data-title') || ('#' + this.currentSchemaRevisionRubricId)); } if (subtitle) { subtitle.textContent = 'Поля, группы, раскладка и условия формы. Значения документов в снимок не входят.'; } this.resetSchemaRevisionPreview(); if (Adminx.Drawer) { Adminx.Drawer.open('rubricSchemaRevisionsDrawer'); } this.refreshSchemaRevisions(); }, refreshSchemaRevisions: function () { if (!this.currentSchemaRevisionRubricId) { return; } var self = this; Adminx.Loader.show(); Adminx.Ajax.request(this.base() + '/rubrics/' + this.currentSchemaRevisionRubricId + '/schema-revisions').then(function (payload) { Adminx.Loader.hide(); var response = payload.data || {}; if (!response.success || !response.data) { Adminx.Toast.show(response.message || 'Не удалось загрузить ревизии', 'error'); return; } self.renderSchemaRevisions(response.data.revisions || []); }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети при загрузке ревизий', 'error'); }); }, renderSchemaRevisions: function (items) { var list = document.querySelector('[data-schema-revisions-list]'); var count = document.querySelector('[data-schema-revisions-count]'); var clear = document.querySelector('[data-schema-revisions-clear]'); if (count) { count.textContent = items.length ? (items.length + ' снимков') : 'История пока пустая'; } if (clear) { clear.disabled = !items.length; } if (!list) { return; } if (!items.length) { list.innerHTML = '
Ревизий пока нет. Первый снимок появится после изменения полей или конструктора.
'; this.resetSchemaRevisionPreview(); return; } list.innerHTML = items.map(function (item) { return '
' + '' + '
Снимок #' + item.id + '' + esc(item.action_label || item.action) + '
' + '' + esc(item.created_label || '-') + (item.author_name ? ' · ' + esc(item.author_name) : '') + '' + '

' + esc(item.comment || 'Схема рубрики') + '

' + '
' + item.fields_count + ' полей' + item.groups_count + ' групп
' + '' + '
'; }).join(''); this.loadSchemaRevision(items[0].id); }, loadSchemaRevision: function (id) { id = parseInt(id, 10) || 0; if (!id) { return; } var self = this; Adminx.Loader.show(); Adminx.Ajax.request(this.base() + '/rubrics/schema-revisions/' + id).then(function (payload) { Adminx.Loader.hide(); var response = payload.data || {}; if (!response.success || !response.data) { Adminx.Toast.show(response.message || 'Не удалось открыть ревизию', 'error'); return; } self.showSchemaRevision(response.data.revision || {}, response.data.impact || {}); }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети при загрузке ревизии', 'error'); }); }, showSchemaRevision: function (revision, impact) { this.currentSchemaRevisionId = parseInt(revision.id, 10) || 0; this.currentSchemaRevisionFingerprint = impact.fingerprint || ''; document.querySelectorAll('[data-schema-revision-open]').forEach(function (row) { row.classList.toggle('is-active', row.getAttribute('data-schema-revision-open') === String(revision.id)); }); var title = document.querySelector('[data-schema-revision-title]'); var meta = document.querySelector('[data-schema-revision-meta]'); var preview = document.querySelector('[data-schema-revision-preview]'); var remove = document.querySelector('[data-schema-revision-delete]'); var restore = document.querySelector('[data-schema-revision-restore]'); if (title) { title.textContent = 'Снимок #' + revision.id + ' · ' + (revision.action_label || revision.action || 'Схема'); } if (meta) { meta.textContent = (revision.created_label || '-') + (revision.author_name ? ' · ' + revision.author_name : '') + (revision.comment ? ' · ' + revision.comment : ''); } if (preview) { preview.innerHTML = this.renderSchemaImpact(impact); } if (remove) { remove.disabled = !this.currentSchemaRevisionId; } if (restore) { restore.disabled = !this.currentSchemaRevisionId || !impact.can_restore; } }, renderSchemaImpact: function (impact) { var summary = impact.summary || {}; var fields = impact.fields || {}; var groups = impact.groups || {}; var blockers = impact.blockers || []; var conditionText = impact.conditions_before === impact.conditions_after ? (impact.conditions_after ? 'Условия формы останутся включены' : 'Условия формы останутся выключены') : ('Условия формы: ' + (impact.conditions_before ? 'включены' : 'выключены') + ' → ' + (impact.conditions_after ? 'включены' : 'выключены')); var html = '
' + '
' + (blockers.length ? 'Восстановление заблокировано' : 'Данные документов останутся на месте') + '' + '' + (blockers.length ? esc(blockers.join(' · ')) : 'Новые поля не удаляются: они перейдут в библиотеку конструктора. Значения документов не стираются.') + '
' + '
' + this.schemaMetric(summary.fields_restore || 0, 'Вернётся полей', 'cyan', 'ti-arrow-back-up') + this.schemaMetric(summary.fields_change || 0, 'Изменится полей', 'blue', 'ti-pencil') + this.schemaMetric(summary.fields_preserve || 0, 'Сохранится новых', 'green', 'ti-shield-check') + this.schemaMetric(impact.documents_with_values || 0, 'С данными', 'amber', 'ti-file-database') + '
' + '
' + esc(conditionText) + 'Всего документов в рубрике: ' + (impact.documents_total || 0) + '
' + this.renderSchemaDependencies(impact.dependencies || {}) + '
' + this.renderSchemaChangeGroup('Поля', fields) + this.renderSchemaChangeGroup('Группы', groups) + '
'; return html; }, schemaMetric: function (value, label, color, icon) { return '
' + value + '' + esc(label) + '
'; }, renderSchemaDependencies: function (dependencies) { var summary = dependencies.summary || {}; var warnings = dependencies.warnings || []; var sections = [ { key: 'requests', label: 'Запросы', icon: 'ti-filter-code' }, { key: 'templates', label: 'Шаблоны', icon: 'ti-template' }, { key: 'api', label: 'API-контракты', icon: 'ti-api' }, { key: 'modules', label: 'Модули', icon: 'ti-plug-connected' } ]; var total = sections.reduce(function (count, section) { return count + (dependencies[section.key] || []).length; }, 0); var body = sections.map(function (section) { var items = dependencies[section.key] || []; if (!items.length) { return ''; } return '
' + '' + esc(section.label) + '' + items.length + '' + '
' + items.map(function (item) { return '
' + esc(item.title || 'Зависимость') + '' + esc(item.meta || '') + '
' + '
    ' + (item.details || []).map(function (detail) { return '
  • ' + esc(detail) + '
  • '; }).join('') + '
'; }).join('') + '
'; }).join(''); var warningHtml = warnings.length ? '
' + esc(warnings.join(' · ')) + '
' : ''; return '
' + '
Зависимости схемы

' + (total ? 'После восстановления проверьте перечисленные места.' : 'Прямых ссылок на изменяемые поля не найдено.') + '

' + total + '
' + '
' + '' + (summary.requests || 0) + ' запросов' + '' + (summary.templates || 0) + ' шаблонов' + '' + (summary.api || 0) + ' API' + '' + (summary.modules || 0) + ' модулей' + '
' + warningHtml + body + '
'; }, renderSchemaChangeGroup: function (title, changes) { var parts = [ { key: 'restore', label: 'Будут возвращены', tone: 'cyan' }, { key: 'preserve', label: 'Останутся без удаления', tone: 'green' } ]; var content = parts.map(function (part) { var items = changes[part.key] || []; if (!items.length) { return ''; } return '
' + part.label + '
' + items.map(function (item) { return '#' + item.id + '' + esc(item.title || '-') + '' + (item.meta ? '' + esc(item.meta) + '' : '') + ''; }).join('') + '
'; }).join(''); var changed = (changes.change || []).map(function (item) { var details = item.changes || []; return '
' + '
' + esc(item.title || '-') + '' + (item.meta ? '' + esc(item.meta) + '' : '') + '
' + '#' + item.id + '' + details.length + ' изм.
' + '
' + details.map(function (detail) { return '
' + esc(detail.label || 'Настройка') + '' + '' + esc(detail.before || 'Не задано') + '' + '' + '' + esc(detail.after || 'Не задано') + '
'; }).join('') + '
'; }).join(''); var changedBlock = changed ? '
Будут изменены' + changed + '
' : ''; return '
' + esc(title) + '
' + (changedBlock + content || '

Изменений нет.

') + '
'; }, resetSchemaRevisionPreview: function () { this.currentSchemaRevisionId = 0; this.currentSchemaRevisionFingerprint = ''; var preview = document.querySelector('[data-schema-revision-preview]'); var title = document.querySelector('[data-schema-revision-title]'); var meta = document.querySelector('[data-schema-revision-meta]'); var remove = document.querySelector('[data-schema-revision-delete]'); var restore = document.querySelector('[data-schema-revision-restore]'); if (title) { title.textContent = 'Выберите ревизию'; } if (meta) { meta.textContent = 'Здесь появится точный список изменений до восстановления.'; } if (preview) { preview.innerHTML = '
Снимок не выбранВыберите запись в истории выше.
'; } if (remove) { remove.disabled = true; } if (restore) { restore.disabled = true; } }, deleteSchemaRevision: function (id) { id = parseInt(id || this.currentSchemaRevisionId, 10) || 0; if (!id) { return; } var self = this; Adminx.Confirm.open({ kind: 'danger', title: 'Удалить снимок схемы?', message: 'Эту точку восстановления нельзя будет вернуть.', confirmLabel: 'Удалить', onConfirm: function () { Adminx.Loader.show(); Adminx.Ajax.post(self.base() + '/rubrics/schema-revisions/' + id + '/delete').then(function (payload) { Adminx.Loader.hide(); var response = payload.data || {}; if (!response.success) { Adminx.Toast.show(response.message || 'Не удалось удалить снимок', 'error'); return; } Adminx.Toast.show(response.message || 'Снимок удалён', 'success'); self.resetSchemaRevisionPreview(); self.refreshSchemaRevisions(); }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); }); } }); }, clearSchemaRevisions: function () { if (!this.currentSchemaRevisionRubricId) { return; } var self = this; Adminx.Confirm.open({ kind: 'danger', title: 'Удалить всю историю схемы?', message: 'Все точки восстановления этой рубрики будут удалены.', confirmLabel: 'Удалить все', onConfirm: function () { Adminx.Loader.show(); Adminx.Ajax.post(self.base() + '/rubrics/' + self.currentSchemaRevisionRubricId + '/schema-revisions/delete').then(function (payload) { Adminx.Loader.hide(); var response = payload.data || {}; if (!response.success) { Adminx.Toast.show(response.message || 'Не удалось очистить историю', 'error'); return; } Adminx.Toast.show(response.message || 'История очищена', 'success'); self.resetSchemaRevisionPreview(); self.refreshSchemaRevisions(); }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); }); } }); }, restoreSchemaRevision: function () { if (!this.currentSchemaRevisionId || !this.currentSchemaRevisionFingerprint) { return; } var self = this; var id = this.currentSchemaRevisionId; Adminx.Confirm.open({ kind: 'warning', title: 'Восстановить схему рубрики?', message: 'Текущая схема сначала сохранится отдельным снимком. Новые поля и значения документов не удаляются.', confirmLabel: 'Восстановить', onConfirm: function () { var data = new FormData(); data.append('fingerprint', self.currentSchemaRevisionFingerprint); Adminx.Loader.show(); Adminx.Ajax.post(self.base() + '/rubrics/schema-revisions/' + id + '/restore', data).then(function (payload) { Adminx.Loader.hide(); var response = payload.data || {}; if (!response.success) { Adminx.Toast.show(response.message || 'Не удалось восстановить схему', 'error'); return; } Adminx.Toast.show(response.message || 'Схема восстановлена', 'success'); self.resetSchemaRevisionPreview(); self.refreshSchemaRevisions(); if (self.currentRubricId === self.currentSchemaRevisionRubricId) { self.loadFields(); } }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); }); } }); }, fillGroupNew: function () { this.clearErrors(this.groupForm); this.groupForm.reset(); this.groupForm.elements.id.value = ''; this.groupForm.elements.rubric_id.value = this.currentRubricId; this.groupForm.elements.group_condition.value = '{}'; this.hydrateBuilderConditionRoot(this.groupForm.querySelector('[data-builder-condition]'), {}, 0); document.getElementById('rubricGroupTitle').textContent = 'Новая группа'; if (Adminx.Drawer) { Adminx.Drawer.open('rubricGroupDrawer'); } }, fillGroupEdit: function (row) { if (!row) { return; } this.clearErrors(this.groupForm); this.groupForm.elements.id.value = row.getAttribute('data-id'); this.groupForm.elements.rubric_id.value = this.currentRubricId; this.groupForm.elements.group_title.value = row.getAttribute('data-title') || ''; this.groupForm.elements.group_description.value = row.getAttribute('data-description') || ''; var condition = {}; try { condition = JSON.parse(row.getAttribute('data-condition') || '{}'); } catch (e) {} this.hydrateBuilderConditionRoot(this.groupForm.querySelector('[data-builder-condition]'), condition, 0); document.getElementById('rubricGroupTitle').textContent = 'Редактирование группы #' + row.getAttribute('data-id'); }, submitGroup: function (confirmed, fingerprint) { var id = this.groupForm.elements.id.value; var rubricId = this.groupForm.elements.rubric_id.value || this.currentRubricId; var self = this; this.clearErrors(this.groupForm); var conditionRoot = this.groupForm.querySelector('[data-builder-condition]'); var condition = this.captureBuilderConditionRoot(conditionRoot); if (id && !confirmed && JSON.stringify(conditionRoot._conditionOriginal || {}) !== JSON.stringify(condition)) { this.previewGroupCondition(id, condition); return; } this.groupForm.elements.group_condition.value = JSON.stringify(condition); var data = new FormData(this.groupForm); if (fingerprint) { data.append('impact_fingerprint', fingerprint); } this.ajax(this.base() + (id ? '/rubrics/groups/' + id : '/rubrics/' + rubricId + '/groups'), data, function () { Adminx.Toast.show('Группа сохранена', 'success'); if (Adminx.Drawer) { Adminx.Drawer.close('rubricGroupDrawer'); } self.loadFields(); }, function (json) { self.showErrors(self.groupForm, json); }); }, previewGroupCondition: function (id, condition) { var self = this; var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); data.append('group_condition', JSON.stringify(condition || {})); this.ajax(this.base() + '/rubrics/groups/' + id + '/condition/preview', data, function (json) { var impact = (json.data || {}).impact || {}; var persist = function () { self.submitGroup(true, impact.fingerprint || ''); }; if (impact.requires_confirmation) { self.showBuilderImpact(impact, persist); return; } persist(); }); }, deleteGroup: function (row) { if (!row || !window.confirm('Удалить группу? Поля будут перенесены в «Без группы».')) { return; } var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); var self = this; this.ajax(this.base() + '/rubrics/groups/' + row.getAttribute('data-id') + '/delete', data, function () { Adminx.Toast.show('Группа удалена', 'success'); self.loadFields(); }); }, fillGroupSelect: function () { if (!this.fieldForm) { return; } var select = this.fieldForm.querySelector('[data-field-groups-select]'); if (!select) { return; } var value = select.value; select.innerHTML = '' + this.currentGroups.map(function (group) { return ''; }).join(''); select.value = value || '0'; }, dragStart: function (e) { var rubricHandle = e.target.closest('[data-rubric-drag-handle]'); var groupHandle = e.target.closest('[data-group-drag-handle]'); var fieldHandle = e.target.closest('[data-field-drag-handle]'); var paletteField = e.target.closest('[data-builder-palette-item]'); if (rubricHandle) { this.dragRubricRow = rubricHandle.closest('[data-rubric-row]'); e.dataTransfer.effectAllowed = 'move'; } else if (groupHandle) { this.dragGroupNode = groupHandle.closest('[data-group-row]'); e.dataTransfer.effectAllowed = 'move'; } else if (fieldHandle) { this.dragFieldNode = fieldHandle.closest('[data-field-row]'); e.dataTransfer.effectAllowed = 'move'; } else if (paletteField) { this.dragBuilderFieldId = parseInt(paletteField.getAttribute('data-id'), 10) || 0; this.dragFieldNode = null; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(this.dragBuilderFieldId)); } if (this.dragFieldNode) { this.dragFieldNode.classList.add('is-dragging'); } }, dragOver: function (e) { var row = e.target.closest('[data-rubric-row]'); var group = e.target.closest('[data-group-row]'); var field = e.target.closest('[data-field-row]'); var fieldDrop = e.target.closest('[data-field-group-drop]'); var palette = e.target.closest('[data-builder-palette]'); if (this.dragRubricRow && row && row !== this.dragRubricRow) { e.preventDefault(); row.parentNode.insertBefore(this.dragRubricRow, this.before(e, row) ? row : row.nextSibling); } else if (this.dragGroupNode && group && group !== this.dragGroupNode) { e.preventDefault(); group.parentNode.insertBefore(this.dragGroupNode, this.before(e, group) ? group : group.nextSibling); } else if (this.dragFieldNode && (field || fieldDrop)) { e.preventDefault(); if (field && field !== this.dragFieldNode) { field.parentNode.insertBefore(this.dragFieldNode, this.beforeBuilder(e, field) ? field : field.nextSibling); } else if (fieldDrop) { var list = fieldDrop.querySelector('[data-field-sortable]'); if (list && !list.contains(this.dragFieldNode)) { list.appendChild(this.dragFieldNode); } } } else if (this.dragFieldNode && palette) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } else if (this.dragBuilderFieldId && fieldDrop) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }, drop: function (e) { if (this.dragRubricRow) { e.preventDefault(); this.saveRubricOrder(); } else if (this.dragGroupNode) { e.preventDefault(); this.saveGroupOrder(); } else if (this.dragFieldNode && e.target.closest('[data-builder-palette]')) { e.preventDefault(); this.unplaceBuilderField(this.dragFieldNode); } else if (this.dragBuilderFieldId) { var fieldDrop = e.target.closest('[data-field-group-drop]'); if (!fieldDrop) { return; } e.preventDefault(); var targetField = e.target.closest('[data-field-row]'); this.placeBuilderField( this.dragBuilderFieldId, fieldDrop.getAttribute('data-field-group-drop'), targetField && this.beforeBuilder(e, targetField) ? targetField : (targetField ? targetField.nextSibling : null) ); } else if (this.dragFieldNode) { e.preventDefault(); this.saveFieldOrder(); } }, dragEnd: function () { if (this.dragFieldNode) { this.dragFieldNode.classList.remove('is-dragging'); } this.dragRubricRow = null; this.dragGroupNode = null; this.dragFieldNode = null; this.dragBuilderFieldId = 0; }, before: function (event, target) { var rect = target.getBoundingClientRect(); return (event.clientY - rect.top) < rect.height / 2; }, beforeBuilder: function (event, target) { if (!target.closest('[data-builder-canvas]')) { return this.before(event, target); } var rect = target.getBoundingClientRect(); var y = event.clientY - rect.top; if (y < rect.height * 0.3) { return true; } if (y > rect.height * 0.7) { return false; } return event.clientX < rect.left + rect.width / 2; }, saveRubricOrder: function () { var ids = Array.prototype.map.call(document.querySelectorAll('[data-rubric-row]'), function (row) { return row.getAttribute('data-id'); }); var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); data.append('order', JSON.stringify(ids)); this.ajax(this.base() + '/rubrics/reorder', data, function () { Adminx.Toast.show('Порядок рубрик сохранён', 'success'); }); }, saveGroupOrder: function () { var ids = Array.prototype.map.call(document.querySelectorAll('[data-group-row]'), function (row) { return row.getAttribute('data-id'); }); var data = new FormData(); data.append('_csrf', this.form.elements._csrf.value); data.append('order', JSON.stringify(ids)); this.ajax(this.base() + '/rubrics/' + this.currentRubricId + '/groups/reorder', data, function () { Adminx.Toast.show('Порядок групп сохранён', 'success'); }); }, saveFieldOrder: function () { var self = this; document.querySelectorAll('[data-builder-canvas] [data-field-group-drop]').forEach(function (group) { var groupId = parseInt(group.getAttribute('data-field-group-drop'), 10) || 0; group.querySelectorAll('[data-field-row]').forEach(function (row) { row.setAttribute('data-group', String(groupId)); var field = self.builderField(row.getAttribute('data-id')); if (field) { field.rubric_field_group = groupId; } }); }); this.cleanupBuilderEmptyStates(); this.updateBuilderGroupCounts(); if (this.dragFieldNode) { this.selectBuilderField(this.dragFieldNode.getAttribute('data-id')); } this.setBuilderDirty(true); }, copy: function (text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function () { Adminx.Toast.show('Тег скопирован', 'success'); }); } }, setHint: function (selector, text, ok) { var el = document.querySelector(selector); if (!el) { return; } el.textContent = text || ''; el.classList.toggle('is-ok', !!ok); el.classList.toggle('is-error', !!text && !ok); }, clearErrors: function (form) { if (!form) { return; } Array.prototype.forEach.call(form.querySelectorAll('[data-error]'), function (el) { el.textContent = ''; }); }, showErrors: function (form, json) { Adminx.Toast.show((json && json.message) || 'Проверьте форму', 'error'); var errors = (json && json.errors) || {}; Object.keys(errors).forEach(function (name) { var el = form.querySelector('[data-error="' + name + '"]'); if (el) { el.textContent = errors[name]; } }); } }; document.addEventListener('DOMContentLoaded', function () { if (document.querySelector('.rubrics-panel')) { Adminx.Rubrics.init(); } }); })(window, document);