(function (window, document) { 'use strict'; var Adminx = window.Adminx || (window.Adminx = {}); Adminx.PublicPresentations = { form: null, drawer: null, dirty: false, loading: false, needsReload: false, currentRevision: 0, activeEditor: 'draft_item_markup', targetCatalog: {}, init: function () { this.form = document.getElementById('publicPresentationForm'); this.drawer = document.getElementById('publicPresentationDrawer'); if (!this.form || !this.drawer) { return; } this.targetCatalog = this.readJson('[data-presentation-targets]'); this.targetTypeChanged(); var self = this; document.addEventListener('click', function (event) { var target = event.target; if (target.closest('[data-presentation-new]')) { self.openNew(); return; } var edit = target.closest('[data-presentation-edit]'); if (edit) { self.open(Number(edit.getAttribute('data-presentation-edit'))); return; } var history = target.closest('[data-presentation-revisions]'); if (history) { self.open(Number(history.getAttribute('data-presentation-revisions')), 'revisions'); return; } var copy = target.closest('[data-presentation-copy]'); if (copy) { self.copy(Number(copy.getAttribute('data-presentation-copy'))); return; } var remove = target.closest('[data-presentation-delete]'); if (remove) { self.remove(Number(remove.getAttribute('data-presentation-delete'))); return; } var tab = target.closest('[data-presentation-tab]'); if (tab) { self.tab(tab.getAttribute('data-presentation-tab')); return; } if (target.closest('[data-presentation-save]')) { self.save().catch(function () {}); return; } if (target.closest('[data-presentation-publish]')) { self.publish(); return; } if (target.closest('[data-presentation-lint]')) { self.lint(); return; } if (target.closest('[data-presentation-preview]')) { self.preview(); return; } if (target.closest('[data-presentation-diagnose]')) { self.diagnose(); return; } var fullscreen = target.closest('[data-presentation-fullscreen]'); if (fullscreen) { self.fullscreen(fullscreen.getAttribute('data-presentation-fullscreen')); return; } var variable = target.closest('[data-presentation-variable]'); if (variable) { self.insertVariable(variable.getAttribute('data-presentation-variable')); return; } if (target.closest('[data-presentation-assignment-save]')) { self.saveAssignment(); return; } var assignmentDelete = target.closest('[data-presentation-assignment-delete]'); if (assignmentDelete) { self.deleteAssignment(Number(assignmentDelete.getAttribute('data-presentation-assignment-delete'))); return; } var revision = target.closest('[data-presentation-revision]'); if (revision) { self.showRevision(Number(revision.getAttribute('data-presentation-revision'))); return; } if (target.closest('[data-presentation-revision-restore]')) { self.restoreRevision(); return; } if (target.closest('[data-presentation-revision-delete]')) { self.deleteRevision(); return; } if (target.closest('[data-presentation-revisions-clear]')) { self.clearRevisions(); } }); document.addEventListener('click', function (event) { var close = event.target.closest('[data-close-drawer]'); if (!close || !self.drawer.contains(close)) { return; } if (self.dirty) { event.preventDefault(); event.stopImmediatePropagation(); self.confirmDiscard(); return; } if (self.needsReload) { window.location.reload(); } }, true); document.addEventListener('keydown', function (event) { if ((event.ctrlKey || event.metaKey) && String(event.key).toLowerCase() === 's' && self.drawer.classList.contains('show')) { event.preventDefault(); self.save().catch(function () {}); return; } if (event.key === 'Escape' && self.dirty && self.drawer.classList.contains('show')) { event.preventDefault(); event.stopImmediatePropagation(); self.confirmDiscard(); } }, true); this.form.addEventListener('input', function (event) { if (!self.loading && !event.target.closest('.public-presentation-assignment-form')) { self.setDirty(true); } }); this.form.addEventListener('change', function (event) { if (!self.loading && !event.target.closest('.public-presentation-assignment-form') && event.target.name !== 'preview_document_id') { self.setDirty(true); } if (event.target.name === 'assignment_target_type') { self.targetTypeChanged(); } }); this.form.addEventListener('submit', function (event) { event.preventDefault(); self.save().catch(function () {}); }); setTimeout(function () { self.bindEditors(); }, 120); }, base: function () { return this.form.getAttribute('data-base') || ''; }, resource: function () { return this.base() + '/public-site/presentations'; }, revisionResource: function () { return this.base() + '/public-site/presentation-revisions'; }, assignmentResource: function () { return this.base() + '/public-site/presentation-assignments'; }, id: function () { return Number(this.field('id').value) || 0; }, field: function (name) { return this.form.querySelector('[name="' + name + '"]'); }, editor: function (name) { var field = this.field(name); return field && field._adminxCodeMirror ? field._adminxCodeMirror : null; }, request: function (url, options) { return Adminx.Ajax.request(url, options || {}).then(function (response) { var payload = response.data || {}; if (!response.ok || !payload.success) { var error = new Error(payload.message || ('HTTP ' + response.status)); error.payload = payload; throw error; } return payload; }); }, bindEditors: function () { var self = this; ['draft_item_markup', 'draft_wrapper_markup', 'draft_empty_markup', 'draft_css'].forEach(function (name) { var editor = self.editor(name); if (!editor || editor._publicPresentationBound) { return; } editor._publicPresentationBound = true; editor.on('focus', function () { self.activeEditor = name; }); editor.on('change', function () { if (!self.loading) { self.setDirty(true); } }); }); }, openNew: function () { this.loading = true; this.form.reset(); this.field('id').value = ''; this.field('title').value = 'Новое представление'; this.field('code').value = 'presentation_' + String(Date.now()).slice(-8); this.field('kind').value = 'card'; this.setCode('draft_item_markup', this.defaultValue('item')); this.setCode('draft_wrapper_markup', this.defaultValue('wrapper')); this.setCode('draft_empty_markup', this.defaultValue('empty')); this.setCode('draft_css', ''); this.loading = false; this.needsReload = false; this.currentRevision = 0; this.setStatus({is_published: false, version: 0, has_changes: true}); this.renderAssignments([]); this.clearRevisionView(); this.clearPreview(); this.clearDiagnostics(); this.setDirty(false); this.tab('item'); Adminx.Drawer.open('publicPresentationDrawer'); this.refreshEditors(); }, open: function (id, panel) { var self = this; Adminx.Loader.show(); this.request(this.resource() + '/' + id).then(function (payload) { self.fill(payload.data || {}); self.tab(panel || 'item'); Adminx.Drawer.open('publicPresentationDrawer'); self.refreshEditors(); if (panel === 'revisions') { self.loadRevisions(); } }).catch(function (error) { self.fail(error); }).finally(function () { Adminx.Loader.hide(); }); }, fill: function (item) { this.loading = true; ['id', 'title', 'code', 'kind', 'description', 'draft_settings_json'].forEach(function (name) { var field = this.field(name); if (field) { field.value = item[name] == null ? (name === 'draft_settings_json' ? '{}' : '') : item[name]; } }, this); ['draft_item_markup', 'draft_wrapper_markup', 'draft_empty_markup', 'draft_css'].forEach(function (name) { this.setCode(name, item[name] || ''); }, this); this.loading = false; this.needsReload = false; this.currentRevision = 0; this.setStatus(item); this.renderAssignments(item.assignments || []); this.clearRevisionView(); this.clearPreview(); this.clearDiagnostics(); this.setDirty(false); }, defaultValue: function (name) { var field = document.querySelector('[data-presentation-default="' + name + '"]'); return field ? field.value : ''; }, setCode: function (name, value) { var field = this.field(name), editor = this.editor(name); if (field) { field.value = value || ''; } if (editor) { editor.setValue(value || ''); } }, sync: function () { if (Adminx.CodeEditor) { Adminx.CodeEditor.syncAll(this.form); } }, refreshEditors: function () { this.bindEditors(); setTimeout(function () { if (Adminx.CodeEditor) { Adminx.CodeEditor.refreshAll(); } }, 80); }, tab: function (name) { this.drawer.querySelectorAll('[data-presentation-tab]').forEach(function (button) { var active = button.getAttribute('data-presentation-tab') === name; button.classList.toggle('is-active', active); button.setAttribute('aria-selected', active ? 'true' : 'false'); }); this.drawer.querySelectorAll('[data-presentation-panel]').forEach(function (panel) { panel.hidden = panel.getAttribute('data-presentation-panel') !== name; }); if (name === 'item') { this.activeEditor = 'draft_item_markup'; } if (name === 'wrapper') { this.activeEditor = 'draft_wrapper_markup'; } if (name === 'empty') { this.activeEditor = 'draft_empty_markup'; } if (name === 'css') { this.activeEditor = 'draft_css'; } if (name === 'revisions') { this.loadRevisions(); } this.refreshEditors(); }, save: function (quiet) { var self = this; this.sync(); var id = this.id(); this.setSaveState('Сохранение...', ''); return this.request(this.resource() + (id ? '/' + id : ''), { method: 'POST', body: new FormData(this.form) }).then(function (payload) { var savedId = Number((payload.data || {}).id) || id; self.field('id').value = savedId; self.needsReload = true; self.setDirty(false); self.setSaveState('Черновик сохранён', 'ok'); if (!quiet) { Adminx.Toast.show(payload.message || 'Черновик сохранён', 'success'); } return savedId; }).catch(function (error) { self.setSaveState('Не сохранено', 'error'); self.fail(error); throw error; }); }, publish: function () { var self = this; this.save(true).then(function (id) { var data = new FormData(); data.set('_csrf', Adminx.csrf()); return self.request(self.resource() + '/' + id + '/publish', {method: 'POST', body: data}); }).then(function (payload) { self.setStatus(payload.data || {}); self.setSaveState('Опубликовано', 'ok'); self.needsReload = true; Adminx.Toast.show(payload.message || 'Представление опубликовано', 'success'); }).catch(function () {}); }, lint: function () { var self = this; this.sync(); var data = new FormData(this.form); this.setLint('Проверка...', ''); this.request(this.resource() + '/lint', {method: 'POST', body: data}).then(function (payload) { self.setLint(payload.message || 'Twig-синтаксис корректен', 'ok'); }).catch(function (error) { self.setLint(error.message, 'error'); }); }, preview: function () { var self = this; this.sync(); Adminx.Loader.show(); this.request(this.resource() + '/preview', {method: 'POST', body: new FormData(this.form)}).then(function (payload) { var result = payload.data || {}; self.drawer.querySelector('[data-presentation-preview-frame]').srcdoc = result.html || ''; self.drawer.querySelector('[data-presentation-preview-json]').textContent = result.json || '{}'; var state = self.drawer.querySelector('[data-presentation-preview-state] span'); if (state) { state.textContent = result.document && result.document.title ? 'Показан материал «' + result.document.title + '» · ID ' + result.document.id : 'Предпросмотр обновлён.'; } }).catch(function (error) { self.fail(error); }).finally(function () { Adminx.Loader.hide(); }); }, clearPreview: function () { var frame = this.drawer.querySelector('[data-presentation-preview-frame]'); if (frame) { frame.removeAttribute('srcdoc'); } var json = this.drawer.querySelector('[data-presentation-preview-json]'); if (json) { json.textContent = 'После запуска здесь появятся данные, доступные шаблону.'; } var state = this.drawer.querySelector('[data-presentation-preview-state] span'); if (state) { state.textContent = 'Выберите материал и запустите предпросмотр.'; } }, fullscreen: function (name) { var editor = this.editor(name); if (editor && Adminx.CodeEditor) { Adminx.CodeEditor.toggleFullscreen(editor); } }, insertVariable: function (value) { var editorName = this.activeEditor === 'draft_css' ? 'draft_item_markup' : this.activeEditor; var tab = {draft_item_markup: 'item', draft_wrapper_markup: 'wrapper', draft_empty_markup: 'empty'}[editorName] || 'item'; this.tab(tab); var editor = this.editor(editorName); if (editor) { editor.replaceSelection(value || ''); editor.focus(); editor.save(); } }, saveAssignment: function () { var id = this.id(), self = this; if (!id) { Adminx.Toast.show('Сначала сохраните представление', 'warning'); return; } var data = new FormData(); data.set('_csrf', Adminx.csrf()); data.set('context_code', this.field('assignment_context').value); data.set('target_type', this.field('assignment_target_type').value); data.set('target_key', this.field('assignment_target_key').value || '0'); data.set('mode', this.field('assignment_mode').value); this.request(this.resource() + '/' + id + '/assignments', {method: 'POST', body: data}).then(function (payload) { self.renderAssignments((payload.data || {}).assignments || []); self.needsReload = true; Adminx.Toast.show(payload.message || 'Назначение сохранено', 'success'); }).catch(function (error) { self.fail(error); }); }, deleteAssignment: function (id) { var self = this; Adminx.Confirm.open({ kind: 'warning', title: 'Удалить назначение?', message: 'Само представление и публичный шаблон не изменятся.', confirmLabel: 'Удалить', onConfirm: function () { var data = new FormData(); data.set('_csrf', Adminx.csrf()); self.request(self.assignmentResource() + '/' + id + '/delete', {method: 'POST', body: data}).then(function () { self.reloadAssignments(); self.needsReload = true; }).catch(function (error) { self.fail(error); }); } }); }, reloadAssignments: function () { var self = this, id = this.id(); if (!id) { return; } this.request(this.resource() + '/' + id).then(function (payload) { self.renderAssignments((payload.data || {}).assignments || []); }).catch(function (error) { self.fail(error); }); }, renderAssignments: function (items) { var list = this.drawer.querySelector('[data-presentation-assignment-list]'); var contexts = { content_list: 'Список материалов', compact_list: 'Компактный список', slider: 'Слайдер', search_results: 'Результаты поиска', related: 'Похожие материалы', favorites: 'Избранное', viewed: 'Просмотренные материалы', catalog_filters: 'Фильтры каталога', document_page: 'Страница материала' }; if (!items.length) { list.innerHTML = '
Назначений пока нет.
'; return; } var self = this; list.innerHTML = items.map(function (item) { var modeClass = item.mode === 'native' ? 'badge-green' : (item.mode === 'preview' ? 'badge-amber' : 'badge-gray'); var targetClass = item.target_available === false ? ' is-missing' : ''; var target = '' + self.escape(item.target_title || item.target_key) + '' + self.escape(item.target_meta || '') + ''; if (item.target_url) { target = '' + self.escape(item.target_title || item.target_key) + '' + self.escape(item.target_meta || '') + ''; } return '
' + self.escape(contexts[item.context_code] || item.context_code) + '' + self.escape(item.updated_label || '') + '' + target + '' + self.escape(item.mode_label || item.mode) + '
'; }).join(''); }, targetTypeChanged: function () { var type = this.field('assignment_target_type').value || 'global'; var select = this.field('assignment_target_key'); var current = select.value; var items = this.targetCatalog[type] || []; var self = this; select.innerHTML = items.map(function (item) { return ''; }).join(''); if (!items.length) { select.innerHTML = ''; select.disabled = true; } else { select.disabled = false; select.value = items.some(function (item) { return String(item.key) === String(current); }) ? current : items[0].key; } var hint = this.drawer.querySelector('[data-presentation-target-hint]'); if (hint) { hint.textContent = type === 'global' ? 'Общее назначение для всех подходящих мест.' : (items.length ? 'Выберите цель по названию. ID вводить не нужно.' : 'Сначала создайте или включите подходящую цель.'); } }, diagnose: function () { var self = this; this.save(true).then(function (id) { self.sync(); self.setDiagnosticState('Проверяем...', 'loading'); return self.request(self.resource() + '/' + id + '/diagnose', { method: 'POST', body: new FormData(self.form) }); }).then(function (payload) { self.renderDiagnostics(payload.data || {}); self.tab('diagnostics'); }).catch(function (error) { self.setDiagnosticState('Проверка не выполнена', 'error'); self.fail(error); }); }, renderDiagnostics: function (result) { var summary = result.summary || {}; var list = this.drawer.querySelector('[data-presentation-diagnostic-list]'); var self = this; this.setDiagnosticState( result.ready ? (Number(summary.warning || 0) > 0 ? 'Ошибок нет · предупреждений: ' + Number(summary.warning || 0) : 'Готово к публикации') : 'Ошибок: ' + Number(summary.error || 0) + ' · предупреждений: ' + Number(summary.warning || 0), result.ready ? (Number(summary.warning || 0) > 0 ? 'warning' : 'ok') : 'error' ); list.innerHTML = (result.checks || []).map(function (check) { var icon = check.status === 'ok' ? 'ti-circle-check' : (check.status === 'error' ? 'ti-alert-circle' : 'ti-alert-triangle'); var action = check.url ? 'Открыть' : ''; return '
' + self.escape(check.title) + '' + self.escape(check.message) + '' + action + '
'; }).join('') || '
Результатов проверки нет.
'; }, clearDiagnostics: function () { this.setDiagnosticState('Проверка ещё не запускалась', ''); var list = this.drawer.querySelector('[data-presentation-diagnostic-list]'); if (list) { list.innerHTML = '
Сохраните представление и запустите диагностику.
'; } }, setDiagnosticState: function (message, state) { var summary = this.drawer.querySelector('[data-presentation-diagnostic-summary]'); if (!summary) { return; } var badge = state === 'ok' ? 'badge-green' : (state === 'warning' ? 'badge-amber' : (state === 'error' ? 'badge-red' : (state === 'loading' ? 'badge-blue' : 'badge-gray'))); summary.innerHTML = '' + this.escape(message || '') + ''; }, copy: function (id) { var self = this; Adminx.Confirm.open({ kind: 'info', title: 'Создать копию?', message: 'Будет создан новый неопубликованный черновик без назначений.', confirmLabel: 'Создать копию', onConfirm: function () { var data = new FormData(); data.set('_csrf', Adminx.csrf()); self.request(self.resource() + '/' + id + '/copy', {method: 'POST', body: data}).then(function () { window.location.reload(); }).catch(function (error) { self.fail(error); }); } }); }, remove: function (id) { var self = this; Adminx.Confirm.open({ kind: 'error', title: 'Удалить представление?', message: 'Черновик, опубликованная версия и ревизии будут удалены. Назначенное представление удалить нельзя.', confirmLabel: 'Удалить', confirmClass: 'btn-danger', onConfirm: function () { var data = new FormData(); data.set('_csrf', Adminx.csrf()); self.request(self.resource() + '/' + id + '/delete', {method: 'POST', body: data}).then(function () { window.location.reload(); }).catch(function (error) { self.fail(error); }); } }); }, loadRevisions: function () { var id = this.id(), self = this; var list = this.drawer.querySelector('[data-presentation-revision-list]'); if (!id) { list.innerHTML = '
Сначала сохраните представление.
'; return; } list.innerHTML = '
Загрузка...
'; this.request(this.resource() + '/' + id + '/revisions').then(function (payload) { var items = (payload.data || {}).revisions || []; self.drawer.querySelector('[data-presentation-revisions-count]').textContent = items.length ? ('Снимков: ' + items.length) : 'Снимков пока нет.'; self.drawer.querySelector('[data-presentation-revisions-clear]').disabled = !items.length; list.innerHTML = items.length ? items.map(function (item) { return ''; }).join('') : '
Ревизий пока нет.
'; }).catch(function (error) { self.fail(error); }); }, showRevision: function (id) { var self = this; this.request(this.revisionResource() + '/' + id).then(function (payload) { var item = payload.data || {}, snapshot = item.snapshot || {}; self.currentRevision = Number(item.id) || 0; self.drawer.querySelectorAll('[data-presentation-revision]').forEach(function (button) { button.classList.toggle('is-active', Number(button.getAttribute('data-presentation-revision')) === self.currentRevision); }); self.drawer.querySelector('[data-presentation-revision-meta]').innerHTML = '' + self.escape(item.action_label || '') + '' + self.escape(item.created_label || '') + ' · ' + self.escape(item.author_name || 'system') + ''; self.drawer.querySelector('[data-presentation-revision-code]').textContent = snapshot.draft_item_markup || ''; self.drawer.querySelector('[data-presentation-revision-restore]').disabled = false; self.drawer.querySelector('[data-presentation-revision-delete]').disabled = false; }).catch(function (error) { self.fail(error); }); }, restoreRevision: function () { if (!this.currentRevision) { return; } var self = this, revision = this.currentRevision; Adminx.Confirm.open({ kind: 'warning', title: 'Восстановить черновик?', message: 'Опубликованная версия публичного сайта не изменится.', confirmLabel: 'Восстановить', onConfirm: function () { var data = new FormData(); data.set('_csrf', Adminx.csrf()); self.request(self.revisionResource() + '/' + revision + '/restore', {method: 'POST', body: data}).then(function () { self.needsReload = true; self.open(self.id(), 'revisions'); Adminx.Toast.show('Ревизия восстановлена в черновик', 'success'); }).catch(function (error) { self.fail(error); }); } }); }, deleteRevision: function () { if (!this.currentRevision) { return; } var self = this, data = new FormData(); data.set('_csrf', Adminx.csrf()); this.request(this.revisionResource() + '/' + this.currentRevision + '/delete', {method: 'POST', body: data}).then(function () { self.clearRevisionView(); self.loadRevisions(); }).catch(function (error) { self.fail(error); }); }, clearRevisions: function () { var self = this, id = this.id(); if (!id) { return; } Adminx.Confirm.open({ kind: 'error', title: 'Удалить все ревизии?', message: 'Текущий черновик и опубликованная версия сохранятся.', confirmLabel: 'Удалить все', confirmClass: 'btn-danger', onConfirm: function () { var data = new FormData(); data.set('_csrf', Adminx.csrf()); self.request(self.resource() + '/' + id + '/revisions/delete', {method: 'POST', body: data}).then(function () { self.clearRevisionView(); self.loadRevisions(); }).catch(function (error) { self.fail(error); }); } }); }, clearRevisionView: function () { this.currentRevision = 0; var meta = this.drawer.querySelector('[data-presentation-revision-meta]'); var code = this.drawer.querySelector('[data-presentation-revision-code]'); if (meta) { meta.textContent = 'Выберите снимок выше.'; } if (code) { code.textContent = ''; } this.drawer.querySelectorAll('[data-presentation-revision-restore],[data-presentation-revision-delete]').forEach(function (button) { button.disabled = true; }); }, setStatus: function (item) { var badge = this.drawer.querySelector('[data-presentation-status]'); badge.textContent = item.is_published ? ('Опубликовано · v' + (item.version || 1)) : 'Черновик'; badge.className = 'badge ' + (item.is_published ? (item.has_changes ? 'badge-amber' : 'badge-green') : 'badge-gray'); this.drawer.querySelector('[data-presentation-title]').textContent = item.title || this.field('title').value || 'Новое представление'; }, setDirty: function (value) { this.dirty = !!value; this.drawer.classList.toggle('has-unsaved-changes', this.dirty); if (this.dirty) { this.setSaveState('Есть несохранённые изменения', 'dirty'); } }, setSaveState: function (message, state) { var element = this.drawer.querySelector('[data-presentation-save-state]'); element.textContent = message || ''; element.className = 'public-presentation-save-state' + (state ? ' is-' + state : ''); }, setLint: function (message, state) { this.drawer.querySelectorAll('[data-presentation-lint-result]').forEach(function (element) { element.textContent = message || ''; element.className = 'field-hint' + (state ? ' is-' + state : ''); }); }, confirmDiscard: function () { var self = this; Adminx.Confirm.open({ kind: 'warning', title: 'Закрыть без сохранения?', message: 'Изменения текущего черновика будут потеряны.', confirmLabel: 'Закрыть', onConfirm: function () { self.setDirty(false); if (self.needsReload) { window.location.reload(); return; } Adminx.Drawer.close(self.drawer); } }); }, readJson: function (selector) { var node = document.querySelector(selector); if (!node) { return {}; } try { return JSON.parse(node.textContent || '{}'); } catch (error) { return {}; } }, escape: function (value) { var node = document.createElement('div'); node.textContent = value == null ? '' : String(value); return node.innerHTML; }, fail: function (error) { Adminx.Toast.show((error && error.message) || 'Не удалось выполнить действие', 'error'); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { Adminx.PublicPresentations.init(); }); } else { Adminx.PublicPresentations.init(); } })(window, document);