mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 00:45:44 +00:00
3469 lines
173 KiB
JavaScript
3469 lines
173 KiB
JavaScript
/**
|
||
* JS раздела «Документы»: AJAX-фильтры, быстрые действия списка и сохранение страницы редактирования.
|
||
*/
|
||
(function (window, document) {
|
||
'use strict';
|
||
|
||
var Adminx = window.Adminx || (window.Adminx = {});
|
||
|
||
Adminx.Documents = {
|
||
form: null,
|
||
filterTimer: null,
|
||
filterAbort: null,
|
||
filterRequest: 0,
|
||
aliasTimer: null,
|
||
shortAliasTimer: null,
|
||
dirty: false,
|
||
submitting: false,
|
||
aliasState: 'empty',
|
||
currentRevisionId: 0,
|
||
currentRevisionDocumentId: 0,
|
||
currentAliasDocumentId: 0,
|
||
currentRemarkDocumentId: 0,
|
||
draftTimer: null,
|
||
draftCandidate: null,
|
||
workspacePanel: 'main',
|
||
errorTargets: [],
|
||
conditionsInitialized: false,
|
||
|
||
init: function () {
|
||
this.form = document.getElementById('documentForm');
|
||
var self = this;
|
||
|
||
document.addEventListener('click', function (e) {
|
||
if (e.target.closest('[data-documents-filter-reset]')) { self.resetFilters(); }
|
||
|
||
var page = e.target.closest('[data-documents-page]');
|
||
if (page) {
|
||
e.preventDefault();
|
||
self.applyFilterUrl(page.href, true);
|
||
}
|
||
|
||
var del = e.target.closest('[data-document-delete]');
|
||
if (del) { self.remove(del.closest('[data-document-row]')); }
|
||
|
||
var restore = e.target.closest('[data-document-restore]');
|
||
if (restore) { self.restore(restore.closest('[data-document-row]')); }
|
||
|
||
var toggle = e.target.closest('[data-document-toggle]');
|
||
if (toggle) { self.action(toggle.closest('[data-document-row]'), '/toggle'); }
|
||
|
||
var copy = e.target.closest('[data-document-copy]');
|
||
if (copy) { self.copy(copy.closest('[data-document-row]')); }
|
||
|
||
var purge = e.target.closest('[data-document-purge]');
|
||
if (purge) { self.purge(purge.closest('[data-document-row]')); }
|
||
|
||
if (e.target.closest('[data-document-views-clear]')) { self.clearViews(); }
|
||
if (e.target.closest('[data-documents-bulk-apply]')) { self.applyBulk(); }
|
||
if (e.target.closest('[data-documents-bulk-clear]')) { self.clearBulk(); }
|
||
if (e.target.closest('[data-documents-snapshots-rebuild]')) { self.rebuildSnapshots(); }
|
||
|
||
var createItem = e.target.closest('[data-document-create-item]');
|
||
if (createItem) { self.selectCreateRubric(createItem.getAttribute('data-document-create-item')); }
|
||
var presetDelete = e.target.closest('[data-document-preset-delete]');
|
||
if (presetDelete) { e.preventDefault(); self.deleteCreationPreset(presetDelete); }
|
||
|
||
var fieldTab = e.target.closest('[data-document-field-tab]');
|
||
if (fieldTab) { self.activateFieldGroup(fieldTab.getAttribute('data-document-field-tab')); }
|
||
|
||
var workspaceTab = e.target.closest('[data-document-workspace-tab]');
|
||
if (workspaceTab && workspaceTab.getAttribute('aria-disabled') !== 'true') { self.setWorkspacePanel(workspaceTab.getAttribute('data-document-workspace-tab'), true); }
|
||
var workspaceOpen = e.target.closest('[data-document-workspace-open]');
|
||
if (workspaceOpen) { self.setWorkspacePanel(workspaceOpen.getAttribute('data-document-workspace-open'), true); }
|
||
if (e.target.closest('[data-document-draft-restore]')) { self.restoreLocalDraft(); }
|
||
if (e.target.closest('[data-document-draft-discard]')) { self.discardLocalDraft(); }
|
||
var errorJump = e.target.closest('[data-document-error-jump]');
|
||
if (errorJump) { self.jumpToError(errorJump.getAttribute('data-document-error-jump')); }
|
||
|
||
var mediaPick = e.target.closest('[data-document-media-pick]');
|
||
if (mediaPick) {
|
||
self.openMediaPicker(mediaPick.closest('.documents-picker-input') || mediaPick.closest('[data-document-media-row]') || mediaPick.closest('.documents-single-media-field') || mediaPick.closest('.documents-field-control'));
|
||
}
|
||
|
||
var mediaAdd = e.target.closest('[data-document-media-add]');
|
||
if (mediaAdd) { self.addMediaRow(mediaAdd.closest('[data-document-media-list]')); }
|
||
|
||
var mediaRemove = e.target.closest('[data-document-media-remove]');
|
||
if (mediaRemove) { self.removeMediaRow(mediaRemove.closest('[data-document-media-row]')); }
|
||
|
||
var mediaUp = e.target.closest('[data-document-media-up]');
|
||
if (mediaUp) { self.moveMediaRow(mediaUp.closest('[data-document-media-row]'), -1); }
|
||
|
||
var mediaDown = e.target.closest('[data-document-media-down]');
|
||
if (mediaDown) { self.moveMediaRow(mediaDown.closest('[data-document-media-row]'), 1); }
|
||
|
||
var mediaClear = e.target.closest('[data-document-media-clear]');
|
||
if (mediaClear) { self.clearMediaRows(mediaClear.closest('[data-document-media-list]')); }
|
||
|
||
var mediaUpload = e.target.closest('[data-document-media-upload]');
|
||
if (mediaUpload) { self.openMediaUpload(mediaUpload.closest('[data-document-media-list], [data-document-media-single]')); }
|
||
|
||
var mediaImportFolder = e.target.closest('[data-document-media-import-folder]');
|
||
if (mediaImportFolder) { self.importMediaFolder(mediaImportFolder.closest('[data-document-media-list]')); }
|
||
|
||
var valueAdd = e.target.closest('[data-document-value-add]');
|
||
if (valueAdd) { self.addValueRow(valueAdd.closest('[data-document-value-list]')); }
|
||
|
||
var valueRemove = e.target.closest('[data-document-value-remove]');
|
||
if (valueRemove) { self.removeValueRow(valueRemove.closest('[data-document-value-row]')); }
|
||
|
||
var valueUp = e.target.closest('[data-document-value-up]');
|
||
if (valueUp) { self.moveValueRow(valueUp.closest('[data-document-value-row]'), -1); }
|
||
|
||
var valueDown = e.target.closest('[data-document-value-down]');
|
||
if (valueDown) { self.moveValueRow(valueDown.closest('[data-document-value-row]'), 1); }
|
||
|
||
var valueClear = e.target.closest('[data-document-value-clear]');
|
||
if (valueClear) { self.clearValueRows(valueClear.closest('[data-document-value-list]')); }
|
||
|
||
var relationPick = e.target.closest('[data-document-relation-pick]');
|
||
if (relationPick) { self.openRelationPicker(relationPick.closest('.documents-field-control')); }
|
||
|
||
var relationRemove = e.target.closest('[data-document-relation-remove]');
|
||
if (relationRemove) { self.removePickedRelation(relationRemove.closest('[data-document-relation-token]')); }
|
||
|
||
var relationClear = e.target.closest('[data-document-relation-clear]');
|
||
if (relationClear) {
|
||
var clearSingle = relationClear.closest('[data-document-relation-single]');
|
||
if (clearSingle) {
|
||
var clearInput = clearSingle.querySelector('[data-document-relation-id]');
|
||
if (clearInput) { clearInput.value = ''; clearInput.dispatchEvent(new Event('input', { bubbles: true })); }
|
||
self.fillRelationSingle(clearSingle, '', '');
|
||
}
|
||
}
|
||
|
||
var parentPick = e.target.closest('[data-document-parent-pick]');
|
||
if (parentPick) { self.openRelationPicker(parentPick.closest('.documents-picker-input')); }
|
||
|
||
var docPick = e.target.closest('[data-document-media-doc-pick]');
|
||
if (docPick) {
|
||
var wrap = docPick.closest('.documents-picker-input');
|
||
var linkInput = wrap ? wrap.querySelector('[data-document-media-url]') : null;
|
||
if (linkInput) { self.openRelationPicker(null, { input: linkInput, insert: 'linkAlias' }); }
|
||
}
|
||
|
||
var slugGen = e.target.closest('[data-slug-generate]');
|
||
if (slugGen) { e.preventDefault(); self.generateSlug(slugGen); }
|
||
|
||
var shortGen = e.target.closest('[data-short-generate]');
|
||
if (shortGen) { e.preventDefault(); self.generateShortAlias(shortGen); }
|
||
|
||
var catOption = e.target.closest('[data-document-catalog-option]');
|
||
if (catOption) { e.preventDefault(); self.addCatalogToken(catOption); return; }
|
||
var catRemove = e.target.closest('[data-document-catalog-remove]');
|
||
if (catRemove) { e.preventDefault(); self.removeCatalogToken(catRemove.closest('[data-document-catalog-token]')); return; }
|
||
|
||
if (e.target.closest('[data-document-revisions]')) { self.openRevisions(); }
|
||
if (e.target.closest('[data-document-aliases]')) { self.openAliases(); }
|
||
if (e.target.closest('[data-document-remarks]')) { self.openRemarks(); }
|
||
if (e.target.closest('[data-document-snapshot]')) { self.openSnapshot(); }
|
||
if (e.target.closest('[data-document-snapshot-rebuild]')) { self.rebuildSnapshot(); }
|
||
if (e.target.closest('[data-document-payload-preview]')) { self.previewPayload(); }
|
||
if (e.target.closest('[data-document-payload-copy]')) { self.copyPayload(); }
|
||
var aliasEdit = e.target.closest('[data-document-alias-edit]');
|
||
if (aliasEdit) { self.editAlias(aliasEdit); }
|
||
var aliasDelete = e.target.closest('[data-document-alias-delete]');
|
||
if (aliasDelete) { self.deleteAlias(aliasDelete.getAttribute('data-document-alias-delete')); }
|
||
if (e.target.closest('[data-document-alias-reset]')) { self.resetAliasForm(); }
|
||
var remarkDelete = e.target.closest('[data-document-remark-delete]');
|
||
if (remarkDelete) { self.deleteRemark(remarkDelete.getAttribute('data-document-remark-delete')); }
|
||
var revisionDelete = e.target.closest('[data-document-revision-delete]');
|
||
if (revisionDelete) { self.deleteRevision(revisionDelete.getAttribute('data-document-revision-delete')); return; }
|
||
var revisionOpen = e.target.closest('[data-document-revision-open]');
|
||
if (revisionOpen) { self.loadRevision(revisionOpen.getAttribute('data-document-revision-open')); }
|
||
if (e.target.closest('[data-document-revisions-clear]')) { self.clearRevisions(); }
|
||
if (e.target.closest('[data-document-revision-restore]')) { self.restoreRevision(); }
|
||
|
||
var apiTokenCopy = e.target.closest('[data-api-token-copy]');
|
||
if (apiTokenCopy) { e.preventDefault(); self.copyApiToken(); return; }
|
||
var apiTokenRevoke = e.target.closest('[data-api-token-revoke]');
|
||
if (apiTokenRevoke) { e.preventDefault(); self.revokeApiToken(apiTokenRevoke.closest('[data-api-token-row]')); return; }
|
||
|
||
if (e.target.closest('[data-document-submit-stay]')) { self.submit(true); }
|
||
var conditionUndo = e.target.closest('[data-condition-action-undo]');
|
||
if (conditionUndo) { e.preventDefault(); self.undoDocumentFieldValueAction(conditionUndo.closest('.ax-document-field')); }
|
||
});
|
||
|
||
if (this.form) {
|
||
this.form.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
self.submit(false);
|
||
});
|
||
this.form.addEventListener('input', function (e) {
|
||
if (!e.target.matches('[data-document-term-query]')) { self.setDirty(true); }
|
||
self.clearDocumentFieldValueAction(e.target.closest('.ax-document-field'), true);
|
||
self.applyFieldConditions();
|
||
});
|
||
this.form.addEventListener('change', function (e) { self.setDirty(true); self.clearDocumentFieldValueAction(e.target.closest('.ax-document-field'), true); self.applyFieldConditions(); });
|
||
var aliasInput = this.field('document_alias');
|
||
this.aliasTouched = !!(aliasInput && aliasInput.value.trim());
|
||
this.form.querySelectorAll('[data-document-boolean]').forEach(function (input) { self.updateBoolean(input); });
|
||
this.updateSeo();
|
||
this.updateAliasTemplateHint();
|
||
this.initTermInputs();
|
||
this.applyFieldConditions();
|
||
this.initWorkspace();
|
||
this.setDirty(false);
|
||
window.setTimeout(function () { self.checkLocalDraft(); }, 180);
|
||
window.addEventListener('beforeunload', function (e) {
|
||
if (!self.dirty || self.submitting) { return; }
|
||
e.preventDefault();
|
||
e.returnValue = '';
|
||
});
|
||
}
|
||
|
||
document.addEventListener('change', function (e) {
|
||
if (e.target.matches('[data-document-workspace-select]')) {
|
||
self.setWorkspacePanel(e.target.value, true);
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', function (e) {
|
||
var current = e.target.closest('[data-document-workspace-tab]');
|
||
if (!current || ['ArrowLeft', 'ArrowRight', 'Home', 'End'].indexOf(e.key) === -1) { return; }
|
||
var tabs = Array.prototype.slice.call(document.querySelectorAll('[data-document-workspace-tab]')).filter(function (tab) {
|
||
return tab.getAttribute('aria-disabled') !== 'true';
|
||
});
|
||
if (!tabs.length) { return; }
|
||
e.preventDefault();
|
||
var index = tabs.indexOf(current);
|
||
if (e.key === 'Home') { index = 0; }
|
||
else if (e.key === 'End') { index = tabs.length - 1; }
|
||
else if (e.key === 'ArrowLeft') { index = (index - 1 + tabs.length) % tabs.length; }
|
||
else { index = (index + 1) % tabs.length; }
|
||
self.setWorkspacePanel(tabs[index].getAttribute('data-document-workspace-tab'), true);
|
||
});
|
||
|
||
document.addEventListener('submit', function (e) {
|
||
var presetForm = e.target.closest('[data-document-preset-form]');
|
||
if (presetForm) {
|
||
e.preventDefault();
|
||
self.saveCreationPreset(presetForm);
|
||
return;
|
||
}
|
||
var aliasForm = e.target.closest('[data-document-alias-form]');
|
||
if (aliasForm) {
|
||
e.preventDefault();
|
||
self.submitAlias(aliasForm);
|
||
return;
|
||
}
|
||
var remarkForm = e.target.closest('[data-document-remark-form]');
|
||
if (remarkForm) {
|
||
e.preventDefault();
|
||
self.submitRemark(remarkForm);
|
||
return;
|
||
}
|
||
var apiTokenForm = e.target.closest('[data-api-token-form]');
|
||
if (apiTokenForm) {
|
||
e.preventDefault();
|
||
self.issueApiToken(apiTokenForm);
|
||
return;
|
||
}
|
||
var filter = e.target.closest('.documents-filter');
|
||
if (!filter) { return; }
|
||
e.preventDefault();
|
||
self.applyFilters(filter, true);
|
||
});
|
||
|
||
document.addEventListener('input', function (e) {
|
||
if (e.target.matches('[data-document-create-search]')) {
|
||
self.filterCreateRubrics(e.target.value);
|
||
return;
|
||
}
|
||
if (e.target.matches('[data-document-color-picker]')) {
|
||
var pickerControl = e.target.closest('[data-document-color-control]');
|
||
var colorValue = pickerControl ? pickerControl.querySelector('[data-document-color-value]') : null;
|
||
if (colorValue) { colorValue.value = String(e.target.value || '').toLowerCase(); }
|
||
}
|
||
if (e.target.matches('[data-document-color-value]')) {
|
||
var valueControl = e.target.closest('[data-document-color-control]');
|
||
var colorPicker = valueControl ? valueControl.querySelector('[data-document-color-picker]') : null;
|
||
var color = String(e.target.value || '').trim().toLowerCase();
|
||
if (/^#?[0-9a-f]{3}$/.test(color)) {
|
||
color = color.replace(/^#/, '');
|
||
color = '#' + color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
|
||
} else if (/^#?[0-9a-f]{6}$/.test(color)) {
|
||
color = '#' + color.replace(/^#/, '');
|
||
} else {
|
||
color = '';
|
||
}
|
||
if (colorPicker && color) { colorPicker.value = color; }
|
||
}
|
||
if (self.form && e.target === self.field('document_alias')) {
|
||
self.aliasTouched = true;
|
||
self.scheduleAliasCheck();
|
||
self.updateSeo();
|
||
return;
|
||
}
|
||
if (self.form && e.target === self.field('document_short_alias')) {
|
||
self.scheduleShortAliasCheck();
|
||
return;
|
||
}
|
||
if (self.form && e.target.matches('[data-slug-source]')) {
|
||
self.syncSlugFromTitle();
|
||
self.updateSeo();
|
||
}
|
||
if (self.form && e.target === self.field('document_meta_description')) {
|
||
self.updateSeo();
|
||
}
|
||
var filter = e.target.closest('.documents-filter');
|
||
if (!filter || !e.target.matches('input[type="search"]')) { return; }
|
||
clearTimeout(self.filterTimer);
|
||
self.filterTimer = setTimeout(function () { self.applyFilters(filter, true); }, 350);
|
||
});
|
||
|
||
document.addEventListener('change', function (e) {
|
||
if (e.target.matches('[data-document-revision-group]')) {
|
||
self.toggleRevisionGroup(e.target.getAttribute('data-document-revision-group'), e.target.checked);
|
||
return;
|
||
}
|
||
if (e.target.matches('[data-document-revision-select]')) {
|
||
self.updateRevisionSelection();
|
||
return;
|
||
}
|
||
if (e.target.matches('[data-document-preset-form] [name="include_fields"]')) {
|
||
self.updatePresetAssetsState(e.target.closest('[data-document-preset-form]'));
|
||
}
|
||
if (e.target.matches('[data-document-boolean]')) {
|
||
self.updateBoolean(e.target);
|
||
}
|
||
if (e.target.matches('[data-documents-check-all]')) {
|
||
document.querySelectorAll('[data-document-check]').forEach(function (box) { box.checked = e.target.checked; });
|
||
self.updateBulk();
|
||
return;
|
||
}
|
||
if (e.target.matches('[data-document-check]')) { self.updateBulk(); return; }
|
||
if (e.target.matches('[data-document-view-days]')) {
|
||
self.loadViews(e.target.value, true);
|
||
return;
|
||
}
|
||
if (e.target.matches('[data-document-rubric-select]')) {
|
||
self.changeCreateRubric(e.target.value);
|
||
return;
|
||
}
|
||
if (self.form && e.target === self.field('document_published') && !self.aliasTouched) {
|
||
self.syncSlugFromTitle();
|
||
}
|
||
var filter = e.target.closest('.documents-filter');
|
||
if (!filter || !e.target.matches('select')) { return; }
|
||
self.applyFilters(filter, true);
|
||
});
|
||
|
||
document.addEventListener('change', function (e) {
|
||
if (!e.target.matches('[data-document-media-files]')) { return; }
|
||
self.uploadMediaFiles(e.target.closest('[data-document-media-list], [data-document-media-single]'), e.target.files);
|
||
e.target.value = '';
|
||
});
|
||
|
||
document.addEventListener('dragover', function (e) {
|
||
var drop = e.target.closest('[data-document-media-drop]');
|
||
if (!drop) { return; }
|
||
e.preventDefault();
|
||
drop.classList.add('is-dragover');
|
||
});
|
||
|
||
document.addEventListener('dragleave', function (e) {
|
||
var drop = e.target.closest('[data-document-media-drop]');
|
||
if (drop) { drop.classList.remove('is-dragover'); }
|
||
});
|
||
|
||
document.addEventListener('drop', function (e) {
|
||
var drop = e.target.closest('[data-document-media-drop]');
|
||
if (!drop) { return; }
|
||
e.preventDefault();
|
||
drop.classList.remove('is-dragover');
|
||
self.uploadMediaFiles(drop.closest('[data-document-media-list], [data-document-media-single]'), e.dataTransfer ? e.dataTransfer.files : null);
|
||
});
|
||
|
||
// drag-and-drop сортировка строк медиа/значений (по ручке-грипу)
|
||
document.addEventListener('dragstart', function (e) { self.rowDragStart(e); });
|
||
document.addEventListener('dragover', function (e) { self.rowDragOver(e); });
|
||
document.addEventListener('drop', function (e) { if (self.dragRow) { e.preventDefault(); } });
|
||
document.addEventListener('dragend', function () { self.rowDragEnd(); });
|
||
|
||
document.addEventListener('blur', function (e) {
|
||
if (!self.form) { return; }
|
||
if (e.target === self.field('document_alias')) { self.checkAlias(false); }
|
||
if (e.target === self.field('document_short_alias')) { self.checkShortAlias(false); }
|
||
}, true);
|
||
|
||
window.addEventListener('popstate', function () {
|
||
if (document.querySelector('.documents-views-panel')) {
|
||
var params = new URLSearchParams(window.location.search);
|
||
self.loadViews(params.get('days') || 30, false);
|
||
return;
|
||
}
|
||
self.applyFilterUrl(window.location.href, false);
|
||
});
|
||
|
||
document.addEventListener('keydown', function (e) {
|
||
if ((e.ctrlKey || e.metaKey) && String(e.key || '').toLowerCase() === 's' && self.form) {
|
||
e.preventDefault();
|
||
self.saveActiveWorkspace();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('input', function (e) {
|
||
if (e.target.matches('[data-document-catalog-search]')) {
|
||
self.searchCatalog(e.target.closest('[data-document-catalog-field]'), e.target.value);
|
||
return;
|
||
}
|
||
if (e.target.matches('[data-document-media-url]')) { self.updateMediaPreview(e.target); }
|
||
});
|
||
|
||
document.querySelectorAll('[data-document-catalog-field]').forEach(function (field) { self.updateCatalogFields(field); });
|
||
|
||
var createDrawer = document.querySelector('[data-documents-create-auto]');
|
||
if (createDrawer && Adminx.Drawer) { Adminx.Drawer.open(createDrawer.id); }
|
||
document.querySelectorAll('[data-document-preset-form]').forEach(function (form) { self.updatePresetAssetsState(form); });
|
||
|
||
document.addEventListener('adminx:drawer:closed', function (e) {
|
||
var drawer = e.detail ? e.detail.drawer : null;
|
||
if (!drawer || drawer.id !== 'documentApiTokenDrawer') { return; }
|
||
if (drawer.getAttribute('data-token-created') === '1') {
|
||
window.location.reload();
|
||
return;
|
||
}
|
||
self.resetApiTokenForm();
|
||
});
|
||
},
|
||
|
||
base: function () { return (this.form && this.form.getAttribute('data-base')) || Adminx.base(); },
|
||
field: function (name) { return this.form.querySelector('[name="' + name + '"]'); },
|
||
|
||
initTermInputs: function () {
|
||
var self = this;
|
||
if (!this.form) { return; }
|
||
this.form.querySelectorAll('[data-document-term-input]').forEach(function (field) {
|
||
if (field._documentTermReady) { return; }
|
||
field._documentTermReady = true;
|
||
field._termItems = self.normalizeTerms((field.querySelector('[data-document-term-value]') || {}).value || '');
|
||
field._termActive = -1;
|
||
field._termOpen = false;
|
||
self.renderTermChips(field);
|
||
|
||
var box = field.querySelector('[data-document-term-box]');
|
||
var input = field.querySelector('[data-document-term-query]');
|
||
var list = field.querySelector('[data-document-term-options]');
|
||
if (!box || !input || !list) { return; }
|
||
|
||
box.addEventListener('click', function (e) {
|
||
if (e.target === box) { input.focus(); }
|
||
});
|
||
input.addEventListener('focus', function () {
|
||
field._termOpen = true;
|
||
if (String(input.value || '').trim()) {
|
||
self.searchTermSuggestions(field, input.value, true);
|
||
}
|
||
});
|
||
input.addEventListener('input', function () {
|
||
if (!String(input.value || '').trim()) {
|
||
self.closeTermSuggestions(field);
|
||
return;
|
||
}
|
||
field._termOpen = true;
|
||
clearTimeout(field._termTimer);
|
||
field._termTimer = setTimeout(function () { self.searchTermSuggestions(field, input.value, true); }, 180);
|
||
});
|
||
input.addEventListener('keydown', function (e) { self.termKeydown(field, e); });
|
||
input.addEventListener('paste', function () {
|
||
setTimeout(function () {
|
||
if (!/[,;\n\r]/.test(input.value)) { return; }
|
||
self.addTerms(field, input.value);
|
||
input.value = '';
|
||
self.closeTermSuggestions(field);
|
||
}, 0);
|
||
});
|
||
field.addEventListener('click', function (e) {
|
||
var remove = e.target.closest('[data-document-term-remove]');
|
||
if (remove) {
|
||
e.preventDefault();
|
||
self.removeTerm(field, remove.getAttribute('data-document-term-remove'));
|
||
input.focus();
|
||
return;
|
||
}
|
||
var option = e.target.closest('[data-document-term-option]');
|
||
if (option) {
|
||
e.preventDefault();
|
||
self.addTerms(field, option.getAttribute('data-value') || '');
|
||
input.value = '';
|
||
self.closeTermSuggestions(field);
|
||
}
|
||
});
|
||
list.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
||
field.addEventListener('focusout', function () {
|
||
setTimeout(function () {
|
||
if (!field.contains(document.activeElement)) { self.closeTermSuggestions(field); }
|
||
}, 0);
|
||
});
|
||
document.addEventListener('click', function (e) {
|
||
if (!field.contains(e.target)) { self.closeTermSuggestions(field); }
|
||
});
|
||
});
|
||
},
|
||
|
||
normalizeTerms: function (value) {
|
||
var terms = Array.isArray(value) ? value : String(value || '').split(/[,;\n\r]+/);
|
||
var seen = {};
|
||
return terms.reduce(function (result, term) {
|
||
term = String(term || '').replace(/[\x00-\x1f\x7f]/g, '').trim();
|
||
var key = term.toLowerCase();
|
||
if (term && !seen[key]) { seen[key] = true; result.push(term.slice(0, 254)); }
|
||
return result;
|
||
}, []);
|
||
},
|
||
|
||
renderTermChips: function (field) {
|
||
var self = this;
|
||
var box = field.querySelector('[data-document-term-box]');
|
||
var input = field.querySelector('[data-document-term-query]');
|
||
var hidden = field.querySelector('[data-document-term-value]');
|
||
if (!box || !input || !hidden) { return; }
|
||
box.querySelectorAll('[data-document-term-chip]').forEach(function (chip) { chip.remove(); });
|
||
(field._termItems || []).forEach(function (term) {
|
||
var chip = document.createElement('span');
|
||
chip.className = 'chip documents-term-chip' + (field.getAttribute('data-term-kind') === 'tags' ? ' chip-teal' : '');
|
||
chip.setAttribute('data-document-term-chip', '');
|
||
var label = document.createElement('span');
|
||
label.textContent = term;
|
||
var remove = document.createElement('button');
|
||
remove.type = 'button';
|
||
remove.setAttribute('data-document-term-remove', term);
|
||
remove.setAttribute('aria-label', 'Удалить «' + term + '»');
|
||
remove.innerHTML = '<i class="ti ti-x" aria-hidden="true"></i>';
|
||
chip.appendChild(label);
|
||
chip.appendChild(remove);
|
||
box.insertBefore(chip, input);
|
||
});
|
||
hidden.value = (field._termItems || []).join(', ');
|
||
box.classList.toggle('is-empty', !(field._termItems || []).length);
|
||
input.setAttribute('aria-label', field.getAttribute('data-term-kind') === 'tags' ? 'Добавить тег' : 'Добавить ключевое слово');
|
||
self.positionTermInput(field);
|
||
},
|
||
|
||
positionTermInput: function (field) {
|
||
var input = field.querySelector('[data-document-term-query]');
|
||
if (input) { input.placeholder = (field._termItems || []).length ? 'Добавить ещё…' : (field.getAttribute('data-term-kind') === 'tags' ? 'Найти или добавить тег' : 'Найти или добавить ключевое слово'); }
|
||
},
|
||
|
||
addTerms: function (field, value) {
|
||
var additions = this.normalizeTerms(value);
|
||
if (!additions.length) { return false; }
|
||
var items = field._termItems || [];
|
||
var seen = {};
|
||
items.forEach(function (item) { seen[String(item).toLowerCase()] = true; });
|
||
var changed = false;
|
||
additions.forEach(function (item) {
|
||
var key = item.toLowerCase();
|
||
if (!seen[key]) { items.push(item); seen[key] = true; changed = true; }
|
||
});
|
||
if (!changed) { return false; }
|
||
field._termItems = items;
|
||
this.renderTermChips(field);
|
||
this.setDirty(true);
|
||
return true;
|
||
},
|
||
|
||
removeTerm: function (field, value) {
|
||
var key = String(value || '').toLowerCase();
|
||
var next = (field._termItems || []).filter(function (item) { return String(item).toLowerCase() !== key; });
|
||
if (next.length === (field._termItems || []).length) { return; }
|
||
field._termItems = next;
|
||
this.renderTermChips(field);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
termKeydown: function (field, e) {
|
||
var input = field.querySelector('[data-document-term-query]');
|
||
var options = Array.prototype.slice.call(field.querySelectorAll('[data-document-term-option]'));
|
||
if (!input) { return; }
|
||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||
if (!String(input.value || '').trim()) { return; }
|
||
e.preventDefault();
|
||
if (!field._termOpen) { field._termOpen = true; this.searchTermSuggestions(field, input.value, true); }
|
||
if (!options.length) { return; }
|
||
var step = e.key === 'ArrowDown' ? 1 : -1;
|
||
field._termActive = Math.max(0, Math.min(options.length - 1, (field._termActive < 0 ? (step > 0 ? -1 : options.length) : field._termActive) + step));
|
||
options.forEach(function (option, index) { option.setAttribute('aria-selected', index === field._termActive ? 'true' : 'false'); });
|
||
options[field._termActive].scrollIntoView({ block: 'nearest' });
|
||
return;
|
||
}
|
||
if (e.key === 'Enter' || e.key === ',') {
|
||
var option = options[field._termActive];
|
||
var value = option ? option.getAttribute('data-value') : input.value;
|
||
if (!String(value || '').trim()) { return; }
|
||
e.preventDefault();
|
||
this.addTerms(field, value);
|
||
input.value = '';
|
||
this.closeTermSuggestions(field);
|
||
return;
|
||
}
|
||
if (e.key === 'Backspace' && !input.value && (field._termItems || []).length) {
|
||
this.removeTerm(field, field._termItems[field._termItems.length - 1]);
|
||
return;
|
||
}
|
||
if (e.key === 'Escape') {
|
||
e.preventDefault();
|
||
this.closeTermSuggestions(field);
|
||
}
|
||
},
|
||
|
||
searchTermSuggestions: function (field, query, immediate) {
|
||
var self = this;
|
||
var list = field.querySelector('[data-document-term-options]');
|
||
var input = field.querySelector('[data-document-term-query]');
|
||
if (!list || !input || !field._termOpen) { return; }
|
||
if (!String(query || '').trim()) {
|
||
this.closeTermSuggestions(field);
|
||
return;
|
||
}
|
||
clearTimeout(field._termTimer);
|
||
if (!immediate) {
|
||
field._termTimer = setTimeout(function () { self.searchTermSuggestions(field, query, true); }, 180);
|
||
return;
|
||
}
|
||
if (field._termAbort && typeof field._termAbort.abort === 'function') { field._termAbort.abort(); }
|
||
field._termAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||
field._termRequest = (field._termRequest || 0) + 1;
|
||
var requestId = field._termRequest;
|
||
this.renderTermLoading(field);
|
||
var params = new URLSearchParams({
|
||
kind: field.getAttribute('data-term-kind') || 'tags',
|
||
q: String(query || '').trim(),
|
||
rubric_id: field.getAttribute('data-term-rubric-id') || '0',
|
||
limit: '12'
|
||
});
|
||
fetch((field.getAttribute('data-term-url') || (this.base() + '/documents/terms')) + '?' + params.toString(), {
|
||
headers: { 'Accept': 'application/json' },
|
||
credentials: 'same-origin',
|
||
signal: field._termAbort ? field._termAbort.signal : undefined
|
||
})
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
if (requestId !== field._termRequest || !field._termOpen) { return; }
|
||
self.renderTermSuggestions(field, ((payload.data || {}).items || []), query);
|
||
})
|
||
.catch(function (error) {
|
||
if (!error || error.name !== 'AbortError') { self.renderTermError(field); }
|
||
});
|
||
},
|
||
|
||
renderTermLoading: function (field) {
|
||
var list = field.querySelector('[data-document-term-options]');
|
||
if (!list) { return; }
|
||
list.innerHTML = '<div class="documents-term-status"><i class="ti ti-loader-2"></i><span>Ищем совпадения…</span></div>';
|
||
this.openTermSuggestions(field);
|
||
},
|
||
|
||
renderTermError: function (field) {
|
||
var list = field.querySelector('[data-document-term-options]');
|
||
if (!list || !field._termOpen) { return; }
|
||
list.innerHTML = '<div class="documents-term-status is-error"><i class="ti ti-alert-circle"></i><span>Не удалось загрузить варианты</span></div>';
|
||
this.openTermSuggestions(field);
|
||
},
|
||
|
||
renderTermSuggestions: function (field, items, query) {
|
||
var self = this;
|
||
var list = field.querySelector('[data-document-term-options]');
|
||
if (!list) { return; }
|
||
list.innerHTML = '';
|
||
field._termActive = -1;
|
||
var selected = {};
|
||
(field._termItems || []).forEach(function (item) { selected[String(item).toLowerCase()] = true; });
|
||
var exact = false;
|
||
(items || []).forEach(function (item) {
|
||
var value = String(item.value || '').trim();
|
||
if (!value) { return; }
|
||
if (value.toLowerCase() === String(query || '').trim().toLowerCase()) { exact = true; }
|
||
if (selected[value.toLowerCase()]) { return; }
|
||
list.appendChild(self.termOption(value, query, item, false));
|
||
});
|
||
query = String(query || '').trim();
|
||
if (query && !exact && !selected[query.toLowerCase()]) {
|
||
list.appendChild(this.termOption(query, query, {}, true));
|
||
}
|
||
if (!list.querySelector('[data-document-term-option]')) {
|
||
var empty = document.createElement('div');
|
||
empty.className = 'documents-term-status';
|
||
empty.innerHTML = '<i class="ti ti-tags-off"></i><span>' + (query ? 'Совпадений нет' : 'Сохранённых значений пока нет') + '</span>';
|
||
list.appendChild(empty);
|
||
}
|
||
this.openTermSuggestions(field);
|
||
},
|
||
|
||
termOption: function (value, query, item, isNew) {
|
||
var option = document.createElement('button');
|
||
option.type = 'button';
|
||
option.className = 'combo-option documents-term-option' + (isNew ? ' is-new' : '');
|
||
option.setAttribute('role', 'option');
|
||
option.setAttribute('aria-selected', 'false');
|
||
option.setAttribute('data-document-term-option', '');
|
||
option.setAttribute('data-value', value);
|
||
var icon = document.createElement('i');
|
||
icon.className = isNew ? 'ti ti-plus' : 'ti ti-tag';
|
||
icon.setAttribute('aria-hidden', 'true');
|
||
var label = document.createElement('span');
|
||
label.className = 'documents-term-option-label';
|
||
if (isNew) {
|
||
label.appendChild(document.createTextNode('Добавить «' + value + '»'));
|
||
} else {
|
||
this.highlightTerm(label, value, query);
|
||
}
|
||
option.appendChild(icon);
|
||
option.appendChild(label);
|
||
var meta = document.createElement('span');
|
||
meta.className = 'co-sub';
|
||
meta.textContent = isNew ? 'новое' : String(item.rubric_count > 0 ? item.rubric_count + ' в рубрике' : item.count + ' док.');
|
||
option.appendChild(meta);
|
||
return option;
|
||
},
|
||
|
||
highlightTerm: function (target, value, query) {
|
||
var index = String(value).toLowerCase().indexOf(String(query || '').trim().toLowerCase());
|
||
if (index < 0 || !String(query || '').trim()) { target.textContent = value; return; }
|
||
var length = String(query).trim().length;
|
||
target.appendChild(document.createTextNode(value.slice(0, index)));
|
||
var mark = document.createElement('mark');
|
||
mark.textContent = value.slice(index, index + length);
|
||
target.appendChild(mark);
|
||
target.appendChild(document.createTextNode(value.slice(index + length)));
|
||
},
|
||
|
||
openTermSuggestions: function (field) {
|
||
var list = field.querySelector('[data-document-term-options]');
|
||
var input = field.querySelector('[data-document-term-query]');
|
||
if (!list || !input || !field._termOpen) { return; }
|
||
list.hidden = false;
|
||
list.classList.add('open');
|
||
input.setAttribute('aria-expanded', 'true');
|
||
},
|
||
|
||
closeTermSuggestions: function (field) {
|
||
var list = field.querySelector('[data-document-term-options]');
|
||
var input = field.querySelector('[data-document-term-query]');
|
||
field._termOpen = false;
|
||
field._termActive = -1;
|
||
if (list) { list.hidden = true; list.classList.remove('open'); }
|
||
if (input) { input.setAttribute('aria-expanded', 'false'); }
|
||
},
|
||
|
||
filterUrl: function (form) {
|
||
var params = new URLSearchParams(new FormData(form));
|
||
params.delete('page');
|
||
Array.from(params.keys()).forEach(function (key) {
|
||
if (String(params.get(key) || '') === '' || String(params.get(key)) === '0') { params.delete(key); }
|
||
});
|
||
var query = params.toString();
|
||
return (form.getAttribute('action') || (this.base() + '/documents')) + (query ? '?' + query : '');
|
||
},
|
||
|
||
applyFilters: function (form, push) {
|
||
if (!form) { return; }
|
||
clearTimeout(this.filterTimer);
|
||
this.filterTimer = null;
|
||
this.applyFilterUrl(this.filterUrl(form), push);
|
||
},
|
||
|
||
applyFilterUrl: function (url, push) {
|
||
var self = this;
|
||
var requestId = ++this.filterRequest;
|
||
var active = document.activeElement;
|
||
var focus = active && active.closest && active.closest('.documents-filter') ? {
|
||
name: active.getAttribute('name') || '',
|
||
start: typeof active.selectionStart === 'number' ? active.selectionStart : null,
|
||
end: typeof active.selectionEnd === 'number' ? active.selectionEnd : null
|
||
} : null;
|
||
if (this.filterAbort && typeof this.filterAbort.abort === 'function') { this.filterAbort.abort(); }
|
||
this.filterAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||
Adminx.Loader.show();
|
||
fetch(url, { method: 'GET', headers: { 'Accept': 'text/html' }, credentials: 'same-origin', signal: this.filterAbort ? this.filterAbort.signal : undefined })
|
||
.then(function (res) {
|
||
return res.text().then(function (html) {
|
||
if (!res.ok) { throw new Error('HTTP ' + res.status); }
|
||
if (requestId === self.filterRequest) { self.replaceList(html, url, push, focus); }
|
||
});
|
||
})
|
||
.catch(function (error) {
|
||
if (!error || error.name !== 'AbortError') { Adminx.Toast.show('Не удалось применить фильтры', 'error'); }
|
||
})
|
||
.finally(function () {
|
||
if (requestId === self.filterRequest) { self.filterAbort = null; Adminx.Loader.hide(); }
|
||
});
|
||
},
|
||
|
||
loadViews: function (days, push) {
|
||
var self = this;
|
||
var url = this.base() + '/documents/views?days=' + encodeURIComponent(days || 30);
|
||
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); } return html; }); })
|
||
.then(function (html) {
|
||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||
var next = doc.querySelector('.documents-views-panel');
|
||
var current = document.querySelector('.documents-views-panel');
|
||
if (!next || !current) { throw new Error('Views panel not found'); }
|
||
current.replaceWith(next);
|
||
if (push) { window.history.pushState({ adminxDocumentViews: true }, '', url); }
|
||
})
|
||
.catch(function () { Adminx.Toast.show('Не удалось загрузить статистику', 'error'); })
|
||
.finally(function () { Adminx.Loader.hide(); });
|
||
},
|
||
|
||
clearViews: function () {
|
||
var self = this;
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Очистить подневную статистику?',
|
||
message: 'Все строки view_count будут удалены. Общие счётчики документов останутся без изменений.',
|
||
confirmLabel: 'Очистить',
|
||
onConfirm: function () {
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/views/clear', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Статистика очищена', 'success');
|
||
var days = document.querySelector('[data-document-view-days]');
|
||
self.loadViews(days ? days.value : 30, false);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
replaceList: function (html, url, push, focus) {
|
||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||
['.documents-summary', '.documents-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({ adminxDocumentsFilters: true }, '', url);
|
||
}
|
||
if (focus && focus.name) {
|
||
var nextFocus = document.querySelector('.documents-filter [name="' + focus.name.replace(/"/g, '\\"') + '"]');
|
||
if (nextFocus) {
|
||
nextFocus.focus();
|
||
if (focus.start !== null && typeof nextFocus.setSelectionRange === 'function') {
|
||
nextFocus.setSelectionRange(focus.start, focus.end);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
resetFilters: function () {
|
||
var form = document.querySelector('.documents-filter');
|
||
if (!form) { return; }
|
||
this.applyFilterUrl(form.getAttribute('action') || (this.base() + '/documents'), true);
|
||
},
|
||
|
||
changeCreateRubric: function (rubricId) {
|
||
if (!this.form || this.field('id').value !== '') { return; }
|
||
this.dirty = false;
|
||
this.submitting = true;
|
||
window.location.href = this.base() + '/documents/create?rubric_id=' + encodeURIComponent(rubricId || '');
|
||
},
|
||
|
||
filterCreateRubrics: function (value) {
|
||
var query = String(value || '').trim().toLowerCase();
|
||
var visible = 0;
|
||
document.querySelectorAll('[data-document-create-item]').forEach(function (item) {
|
||
var match = !query || String(item.getAttribute('data-search') || '').indexOf(query) !== -1;
|
||
item.hidden = !match;
|
||
if (match) { visible++; }
|
||
});
|
||
var empty = document.querySelector('[data-document-create-empty]');
|
||
if (empty) { empty.hidden = visible > 0; }
|
||
var current = document.querySelector('[data-document-create-item].is-current:not([hidden])');
|
||
if (!current) {
|
||
var first = document.querySelector('[data-document-create-item]:not([hidden])');
|
||
if (first) { this.selectCreateRubric(first.getAttribute('data-document-create-item')); }
|
||
}
|
||
},
|
||
|
||
selectCreateRubric: function (rubricId) {
|
||
rubricId = String(rubricId || '');
|
||
document.querySelectorAll('[data-document-create-item]').forEach(function (item) {
|
||
var selected = item.getAttribute('data-document-create-item') === rubricId;
|
||
item.classList.toggle('is-current', selected);
|
||
item.setAttribute('aria-selected', selected ? 'true' : 'false');
|
||
});
|
||
document.querySelectorAll('[data-document-create-detail]').forEach(function (detail) {
|
||
detail.hidden = detail.getAttribute('data-document-create-detail') !== rubricId;
|
||
});
|
||
},
|
||
|
||
deleteCreationPreset: function (button) {
|
||
var id = parseInt(button.getAttribute('data-document-preset-delete'), 10) || 0;
|
||
if (!id) { return; }
|
||
var self = this;
|
||
Adminx.Confirm.open({
|
||
kind: 'error',
|
||
title: 'Удалить пресет создания?',
|
||
message: 'Сохранённые документы не изменятся. Заготовка исчезнет из выбора новых документов.',
|
||
confirmLabel: 'Удалить',
|
||
confirmClass: 'btn-danger',
|
||
onConfirm: function () {
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/presets/' + id + '/delete', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Пресет удалён', 'success');
|
||
var url = new URL(window.location.href);
|
||
url.searchParams.set('create', '1');
|
||
window.location.href = url.toString();
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
saveCreationPreset: function (form) {
|
||
if (!form || form.getAttribute('aria-busy') === 'true') { return; }
|
||
form.querySelectorAll('.field-error').forEach(function (error) { error.textContent = ''; });
|
||
form.setAttribute('aria-busy', 'true');
|
||
var submit = form.querySelector('button[type="submit"]');
|
||
if (submit) { submit.disabled = true; }
|
||
var self = this;
|
||
this.ajax(form.getAttribute('action'), new FormData(form), function (payload) {
|
||
form.removeAttribute('aria-busy');
|
||
if (submit) { submit.disabled = false; }
|
||
form.reset();
|
||
self.updatePresetAssetsState(form);
|
||
if (Adminx.Drawer) { Adminx.Drawer.close('documentPresetDrawer'); }
|
||
Adminx.Toast.show(payload.message || 'Пресет создания сохранён', 'success');
|
||
}, function (payload) {
|
||
form.removeAttribute('aria-busy');
|
||
if (submit) { submit.disabled = false; }
|
||
var errors = payload && payload.errors ? payload.errors : {};
|
||
Object.keys(errors).forEach(function (key) {
|
||
var target = form.querySelector('[data-error="' + key + '"]');
|
||
if (target) { target.textContent = errors[key]; }
|
||
});
|
||
Adminx.Toast.show((payload && payload.message) || 'Не удалось сохранить пресет', 'error');
|
||
return false;
|
||
});
|
||
},
|
||
|
||
updatePresetAssetsState: function (form) {
|
||
if (!form) { return; }
|
||
var fields = form.querySelector('[name="include_fields"]');
|
||
var assets = form.querySelector('[name="include_assets"]');
|
||
if (!assets) { return; }
|
||
assets.disabled = !fields || !fields.checked;
|
||
if (assets.disabled) { assets.checked = false; }
|
||
},
|
||
|
||
activateFieldGroup: function (index) {
|
||
document.querySelectorAll('[data-document-field-tab]').forEach(function (tab) {
|
||
tab.classList.toggle('is-active', tab.getAttribute('data-document-field-tab') === String(index));
|
||
tab.setAttribute('aria-selected', tab.getAttribute('data-document-field-tab') === String(index) ? 'true' : 'false');
|
||
});
|
||
document.querySelectorAll('[data-document-field-panel]').forEach(function (panel) {
|
||
panel.hidden = panel.getAttribute('data-document-field-panel') !== String(index);
|
||
});
|
||
setTimeout(function () {
|
||
if (Adminx.CodeEditor) { Adminx.CodeEditor.refreshAll(); }
|
||
}, 80);
|
||
},
|
||
|
||
applyFieldConditions: function () {
|
||
if (!this.form) { return; }
|
||
var context = this.documentFieldConditionContext();
|
||
var self = this;
|
||
this.form.querySelectorAll('[data-document-field-panel]').forEach(function (panel) {
|
||
var raw = panel.getAttribute('data-group-condition') || '';
|
||
var condition = panel._documentGroupCondition;
|
||
if (condition === undefined) {
|
||
try { condition = raw ? JSON.parse(raw) : null; }
|
||
catch (e) { condition = null; }
|
||
panel._documentGroupCondition = condition;
|
||
}
|
||
var matched = condition && condition.tree ? self.documentConditionMatches(condition.tree, context) : true;
|
||
var mode = condition && condition.mode ? condition.mode : 'show';
|
||
panel._documentGroupState = {
|
||
visible: !condition || !condition.tree || mode === 'lock' ? true : (mode === 'hide' ? !matched : matched),
|
||
locked: !!(condition && condition.tree && matched && mode === 'lock')
|
||
};
|
||
var tab = self.form.querySelector('[data-document-field-tab="' + panel.getAttribute('data-document-field-panel') + '"]');
|
||
if (tab) {
|
||
tab.classList.toggle('is-condition-locked', panel._documentGroupState.locked);
|
||
var marker = tab.querySelector('.documents-group-lock');
|
||
if (panel._documentGroupState.locked && !marker) { tab.insertAdjacentHTML('beforeend', '<i class="ti ti-lock documents-group-lock" aria-label="Только чтение"></i>'); }
|
||
else if (!panel._documentGroupState.locked && marker) { marker.remove(); }
|
||
}
|
||
});
|
||
this.form.querySelectorAll('.ax-document-field[data-rubric-field-id]').forEach(function (field) {
|
||
var raw = field.getAttribute('data-field-condition') || '';
|
||
var condition = field._documentCondition;
|
||
if (condition === undefined) {
|
||
try { condition = raw ? JSON.parse(raw) : null; }
|
||
catch (e) { condition = null; }
|
||
field._documentCondition = condition;
|
||
}
|
||
var matched = condition && condition.tree ? self.documentConditionMatches(condition.tree, context) : true;
|
||
var matchedBefore = field._documentConditionMatched;
|
||
field._documentConditionMatched = matched;
|
||
if (self.conditionsInitialized && matchedBefore === false && matched && condition && ['set', 'clear'].indexOf(String(condition.value_action || '')) !== -1) {
|
||
self.applyDocumentFieldValueAction(field, String(condition.value_action), Object.prototype.hasOwnProperty.call(condition, 'action_value') ? String(condition.action_value) : '');
|
||
}
|
||
var mode = condition && condition.mode ? condition.mode : 'show';
|
||
var fieldVisible = !condition || !condition.tree || mode === 'lock' ? true : (mode === 'hide' ? !matched : matched);
|
||
var group = field.closest('[data-document-field-panel]');
|
||
var groupState = group && group._documentGroupState ? group._documentGroupState : { visible: true, locked: false };
|
||
var visible = groupState.visible && fieldVisible;
|
||
var required = visible && (field.getAttribute('data-base-required') === '1' || !!(condition && condition.required));
|
||
var locked = visible && (groupState.locked || (matched && mode === 'lock'));
|
||
var allowedValues = visible && matched && condition && Array.isArray(condition.allowed_values) ? condition.allowed_values.map(String) : null;
|
||
self.setDocumentFieldConditionState(field, visible, required, locked, allowedValues);
|
||
});
|
||
this.conditionsInitialized = true;
|
||
this.updateCatalogGroups();
|
||
},
|
||
|
||
fieldValueControls: function (field) {
|
||
if (!field) { return []; }
|
||
return Array.prototype.slice.call(field.querySelectorAll('input[name], select[name], textarea[name]')).filter(function (control) {
|
||
return control.type !== 'hidden' && !control.hasAttribute('data-document-term-query');
|
||
});
|
||
},
|
||
|
||
applyDocumentFieldValueAction: function (field, action, value) {
|
||
var controls = this.fieldValueControls(field);
|
||
if (!field || !controls.length) { return; }
|
||
this.setDocumentFieldValueActionOverride(field, false);
|
||
field._conditionActionUndo = controls.map(function (control) {
|
||
return {
|
||
control: control,
|
||
value: control.value,
|
||
checked: !!control.checked,
|
||
selected: control.tagName === 'SELECT' && control.multiple
|
||
? Array.prototype.map.call(control.selectedOptions, function (option) { return option.value; }) : null
|
||
};
|
||
});
|
||
field._conditionActionApplying = true;
|
||
controls.forEach(function (control) {
|
||
if (control.tagName === 'SELECT') {
|
||
if (control.multiple) {
|
||
Array.prototype.forEach.call(control.options, function (option) { option.selected = action === 'set' && String(option.value) === value; });
|
||
} else { control.value = action === 'set' ? value : ''; }
|
||
} else if (control.type === 'radio') {
|
||
control.checked = action === 'set' && String(control.value) === value;
|
||
} else if (control.type === 'checkbox') {
|
||
control.checked = action === 'set' && ['1', 'true', 'yes', 'on'].indexOf(value.toLowerCase()) !== -1;
|
||
} else {
|
||
control.value = action === 'set' ? value : '';
|
||
}
|
||
});
|
||
field._conditionActionApplying = false;
|
||
this.renderDocumentFieldValueAction(field, action);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
renderDocumentFieldValueAction: function (field, action) {
|
||
var header = field ? field.querySelector('.ax-attr-label') : null;
|
||
if (!header) { return; }
|
||
var marker = header.querySelector('.ax-condition-action');
|
||
if (!marker) {
|
||
header.insertAdjacentHTML('beforeend', '<span class="ax-condition-action"><i class="ti ti-wand"></i><span></span><button type="button" data-condition-action-undo aria-label="Отменить изменение" data-tooltip-left="Отменить"><i class="ti ti-arrow-back-up"></i></button></span>');
|
||
marker = header.querySelector('.ax-condition-action');
|
||
}
|
||
var copy = marker.querySelector('span');
|
||
if (copy) { copy.textContent = action === 'clear' ? 'Очищено условием' : 'Установлено условием'; }
|
||
},
|
||
|
||
clearDocumentFieldValueAction: function (field, markOverride) {
|
||
if (!field || field._conditionActionApplying) { return; }
|
||
var hadAction = Array.isArray(field._conditionActionUndo) || !!field.querySelector('.ax-condition-action');
|
||
if (markOverride && hadAction) { this.setDocumentFieldValueActionOverride(field, true); }
|
||
field._conditionActionUndo = null;
|
||
var marker = field.querySelector('.ax-condition-action');
|
||
if (marker) { marker.remove(); }
|
||
},
|
||
|
||
setDocumentFieldValueActionOverride: function (field, enabled) {
|
||
if (!field) { return; }
|
||
var control = field.querySelector('[data-condition-action-override]');
|
||
if (!enabled) {
|
||
if (control) { control.remove(); }
|
||
return;
|
||
}
|
||
if (control) { return; }
|
||
var fieldId = parseInt(field.getAttribute('data-rubric-field-id'), 10) || 0;
|
||
if (fieldId <= 0) { return; }
|
||
control = document.createElement('input');
|
||
control.type = 'hidden';
|
||
control.name = 'fields[' + fieldId + '][_condition_action_override]';
|
||
control.value = '1';
|
||
control.setAttribute('data-condition-action-override', '');
|
||
field.appendChild(control);
|
||
},
|
||
|
||
undoDocumentFieldValueAction: function (field) {
|
||
if (!field || !Array.isArray(field._conditionActionUndo)) { return; }
|
||
field._conditionActionApplying = true;
|
||
field._conditionActionUndo.forEach(function (state) {
|
||
var control = state.control;
|
||
if (!control || !control.isConnected) { return; }
|
||
control.value = state.value;
|
||
if (control.type === 'checkbox' || control.type === 'radio') { control.checked = state.checked; }
|
||
if (control.tagName === 'SELECT' && control.multiple && Array.isArray(state.selected)) {
|
||
Array.prototype.forEach.call(control.options, function (option) { option.selected = state.selected.indexOf(option.value) !== -1; });
|
||
}
|
||
});
|
||
field._conditionActionApplying = false;
|
||
this.clearDocumentFieldValueAction(field, true);
|
||
this.setDirty(true);
|
||
this.applyFieldConditions();
|
||
},
|
||
|
||
documentFieldConditionContext: function () {
|
||
var context = {};
|
||
this.form.querySelectorAll('.ax-document-field[data-rubric-field-id]').forEach(function (field) {
|
||
var values = [];
|
||
var controls = Array.prototype.slice.call(field.querySelectorAll('input[name], select[name], textarea[name]'));
|
||
controls.forEach(function (control) {
|
||
if ((control.type === 'checkbox' || control.type === 'radio') && !control.checked) { return; }
|
||
if (control.type === 'hidden') {
|
||
var choices = controls.filter(function (other) { return other.name === control.name && (other.type === 'checkbox' || other.type === 'radio'); });
|
||
if (choices.some(function (choice) { return choice.checked; })) { return; }
|
||
}
|
||
if (control.tagName === 'SELECT' && control.multiple) {
|
||
Array.prototype.forEach.call(control.selectedOptions, function (option) { values.push(String(option.value)); });
|
||
} else {
|
||
values.push(String(control.value == null ? '' : control.value));
|
||
}
|
||
});
|
||
var id = String(parseInt(field.getAttribute('data-rubric-field-id'), 10) || 0);
|
||
var alias = String(field.getAttribute('data-field-alias') || '').trim().toLowerCase();
|
||
context['id:' + id] = values;
|
||
if (alias) { context['alias:' + alias] = values; }
|
||
});
|
||
return context;
|
||
},
|
||
|
||
documentConditionMatches: function (node, context) {
|
||
if (node && Array.isArray(node.items)) {
|
||
var join = node.operator === 'or' ? 'or' : 'and';
|
||
if (!node.items.length) { return true; }
|
||
for (var i = 0; i < node.items.length; i++) {
|
||
var result = this.documentConditionMatches(node.items[i], context);
|
||
if (join === 'and' && !result) { return false; }
|
||
if (join === 'or' && result) { return true; }
|
||
}
|
||
return join === 'and';
|
||
}
|
||
var values = context[String((node || {}).field || '')] || [''];
|
||
return this.documentConditionCompare(values, String((node || {}).operator || 'equals'), String((node || {}).value || ''));
|
||
},
|
||
|
||
documentConditionCompare: function (values, operator, expected) {
|
||
values = Array.isArray(values) ? values.map(String) : [String(values == null ? '' : values)];
|
||
var nonEmpty = values.filter(function (value) { return value.trim() !== ''; });
|
||
if (operator === 'empty') { return nonEmpty.length === 0; }
|
||
if (operator === 'not_empty') { return nonEmpty.length > 0; }
|
||
if (operator === 'equals') { return values.indexOf(expected) !== -1; }
|
||
if (operator === 'not_equals') { return values.indexOf(expected) === -1; }
|
||
if (operator === 'contains' || operator === 'not_contains') {
|
||
var found = expected !== '' && values.some(function (value) { return value.toLowerCase().indexOf(expected.toLowerCase()) !== -1; });
|
||
return operator === 'contains' ? found : !found;
|
||
}
|
||
if (operator === 'in' || operator === 'not_in') {
|
||
var options = expected.split(/[\r\n,]+/).map(function (value) { return value.trim(); }).filter(Boolean);
|
||
var included = values.some(function (value) { return options.indexOf(value) !== -1; });
|
||
return operator === 'in' ? included : !included;
|
||
}
|
||
var number = nonEmpty.length ? Number(nonEmpty[0].replace(',', '.')) : NaN;
|
||
var target = Number(expected.replace(',', '.'));
|
||
if (!Number.isFinite(number) || !Number.isFinite(target)) { return false; }
|
||
if (operator === 'greater') { return number > target; }
|
||
if (operator === 'greater_or_equal') { return number >= target; }
|
||
if (operator === 'less') { return number < target; }
|
||
if (operator === 'less_or_equal') { return number <= target; }
|
||
return false;
|
||
},
|
||
|
||
setDocumentFieldConditionState: function (field, visible, required, locked, allowedValues) {
|
||
field.hidden = !visible;
|
||
field.classList.toggle('is-condition-hidden', !visible);
|
||
field.classList.toggle('is-condition-locked', !!locked);
|
||
field.classList.toggle('is-required', required);
|
||
field.setAttribute('aria-hidden', visible ? 'false' : 'true');
|
||
field.setAttribute('aria-disabled', locked ? 'true' : 'false');
|
||
field.querySelectorAll('input, select, textarea, button').forEach(function (control) {
|
||
if (control.hasAttribute('data-condition-action-undo')) { return; }
|
||
if ((!visible || locked) && !control.disabled) {
|
||
control.disabled = true;
|
||
control.setAttribute('data-condition-disabled', '1');
|
||
} else if (visible && !locked && control.getAttribute('data-condition-disabled') === '1') {
|
||
control.disabled = false;
|
||
control.removeAttribute('data-condition-disabled');
|
||
}
|
||
});
|
||
this.setDocumentFieldAllowedValues(field, allowedValues);
|
||
field.querySelectorAll('[contenteditable]').forEach(function (control) {
|
||
if ((!visible || locked) && control.getAttribute('contenteditable') !== 'false') {
|
||
control.setAttribute('data-condition-contenteditable', control.getAttribute('contenteditable') || 'true');
|
||
control.setAttribute('contenteditable', 'false');
|
||
} else if (visible && !locked && control.hasAttribute('data-condition-contenteditable')) {
|
||
control.setAttribute('contenteditable', control.getAttribute('data-condition-contenteditable'));
|
||
control.removeAttribute('data-condition-contenteditable');
|
||
}
|
||
});
|
||
var label = field.querySelector('.ax-label');
|
||
var marker = label ? label.querySelector('.ax-required') : null;
|
||
if (required && label && !marker) {
|
||
label.insertAdjacentHTML('beforeend', '<span class="ax-required" title="Обязательное поле" aria-hidden="true">*</span>');
|
||
} else if (!required && marker) {
|
||
marker.remove();
|
||
}
|
||
var header = field.querySelector('.ax-attr-label');
|
||
var lockMarker = header ? header.querySelector('.ax-condition-lock') : null;
|
||
if (locked && header && !lockMarker) {
|
||
header.insertAdjacentHTML('beforeend', '<span class="ax-condition-lock"><i class="ti ti-lock"></i>Только чтение</span>');
|
||
} else if (!locked && lockMarker) {
|
||
lockMarker.remove();
|
||
}
|
||
},
|
||
|
||
setDocumentFieldAllowedValues: function (field, allowedValues) {
|
||
var limited = Array.isArray(allowedValues) && allowedValues.length > 0;
|
||
var allowed = limited ? allowedValues.map(String) : [];
|
||
var invalidSelected = false;
|
||
field.querySelectorAll('select option').forEach(function (option) {
|
||
var unavailable = limited && option.value !== '' && allowed.indexOf(String(option.value)) === -1;
|
||
if (unavailable && option.selected) { invalidSelected = true; }
|
||
option.disabled = unavailable && !option.selected;
|
||
option.classList.toggle('is-condition-unavailable', unavailable);
|
||
});
|
||
field.querySelectorAll('input[type="radio"], input[type="checkbox"]').forEach(function (control) {
|
||
var unavailable = limited && allowed.indexOf(String(control.value)) === -1;
|
||
if (unavailable && control.checked) { invalidSelected = true; }
|
||
if (unavailable && !control.checked) {
|
||
control.disabled = true;
|
||
control.setAttribute('data-condition-option-disabled', '1');
|
||
} else if ((!unavailable || control.checked) && control.getAttribute('data-condition-option-disabled') === '1'
|
||
&& !field.hidden && !field.classList.contains('is-condition-locked')) {
|
||
control.disabled = false;
|
||
control.removeAttribute('data-condition-option-disabled');
|
||
}
|
||
var label = control.closest('label');
|
||
if (label) { label.classList.toggle('is-condition-unavailable', unavailable); }
|
||
});
|
||
var header = field.querySelector('.ax-attr-label');
|
||
var marker = header ? header.querySelector('.ax-condition-options') : null;
|
||
if (limited && header && !marker) {
|
||
header.insertAdjacentHTML('beforeend', '<span class="ax-condition-options"><i class="ti ti-filter"></i><span></span></span>');
|
||
marker = header.querySelector('.ax-condition-options');
|
||
}
|
||
if (marker) {
|
||
marker.hidden = !limited;
|
||
marker.classList.toggle('is-warning', invalidSelected);
|
||
var copy = marker.querySelector('span');
|
||
if (copy) { copy.textContent = invalidSelected ? 'Выберите доступный вариант' : ('Доступно: ' + allowed.length); }
|
||
}
|
||
},
|
||
|
||
updateCatalogFields: function (catalog) {
|
||
if (!catalog) { return; }
|
||
var tokens = catalog.querySelectorAll('[data-document-catalog-token]');
|
||
var limited = catalog.getAttribute('data-catalog-limits-fields') === '1';
|
||
var allowed = {};
|
||
tokens.forEach(function (token) {
|
||
String(token.getAttribute('data-catalog-fields') || '').split(',').forEach(function (id) { if (id) { allowed[String(Number(id))] = true; } });
|
||
});
|
||
document.querySelectorAll('[data-document-catalog-field]').forEach(function (other) {
|
||
var host = other.closest('[data-rubric-field-id]');
|
||
if (host) { allowed[String(host.getAttribute('data-rubric-field-id'))] = true; }
|
||
});
|
||
document.querySelectorAll('.ax-document-field[data-rubric-field-id]').forEach(function (field) {
|
||
var hide = (limited || tokens.length > 0) && !allowed[String(field.getAttribute('data-rubric-field-id'))];
|
||
field.classList.toggle('is-catalog-hidden', hide);
|
||
});
|
||
var empty = catalog.querySelector('[data-document-catalog-empty]');
|
||
if (empty) { empty.hidden = tokens.length > 0; }
|
||
this.updateCatalogGroups();
|
||
},
|
||
|
||
addCatalogToken: function (option) {
|
||
var catalog = option.closest('[data-document-catalog-field]');
|
||
if (!catalog) { return; }
|
||
var tokens = catalog.querySelector('[data-document-catalog-tokens]');
|
||
var fieldId = catalog.getAttribute('data-document-catalog-field');
|
||
var id = option.getAttribute('data-catalog-id');
|
||
if (!tokens || catalog.querySelector('[data-document-catalog-token][data-catalog-id="' + id + '"]')) { return; }
|
||
var name = option.getAttribute('data-catalog-name') || ('#' + id);
|
||
var fields = option.getAttribute('data-catalog-fields') || '';
|
||
var chip = document.createElement('span');
|
||
chip.className = 'documents-catalog-token';
|
||
chip.setAttribute('data-document-catalog-token', '');
|
||
chip.setAttribute('data-catalog-id', id);
|
||
chip.setAttribute('data-catalog-fields', fields);
|
||
chip.innerHTML = '<i class="ti ti-folder documents-catalog-token-icon" aria-hidden="true"></i>'
|
||
+ '<span class="documents-catalog-token-name">' + this.esc(name) + '</span>'
|
||
+ '<input type="hidden" name="fields[' + fieldId + '][catalog_ids][]" value="' + this.esc(id) + '">'
|
||
+ '<button class="documents-catalog-token-remove" type="button" data-document-catalog-remove aria-label="Убрать раздел"><i class="ti ti-x"></i></button>';
|
||
tokens.appendChild(chip);
|
||
option.classList.add('is-picked');
|
||
var dd = catalog.querySelector('.dropdown');
|
||
if (dd) { dd.classList.remove('open'); }
|
||
var search = catalog.querySelector('[data-document-catalog-search]');
|
||
if (search) { search.value = ''; this.searchCatalog(catalog, ''); }
|
||
this.updateCatalogFields(catalog);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
removeCatalogToken: function (token) {
|
||
if (!token) { return; }
|
||
var catalog = token.closest('[data-document-catalog-field]');
|
||
var id = token.getAttribute('data-catalog-id');
|
||
if (token.parentNode) { token.parentNode.removeChild(token); }
|
||
if (!catalog) { return; }
|
||
var option = catalog.querySelector('[data-document-catalog-option][data-catalog-id="' + id + '"]');
|
||
if (option) { option.classList.remove('is-picked'); }
|
||
this.updateCatalogFields(catalog);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
updateCatalogGroups: function () {
|
||
var firstVisible = null;
|
||
document.querySelectorAll('[data-document-field-panel]').forEach(function (panel) {
|
||
var count = panel.querySelectorAll('.ax-document-field:not(.is-catalog-hidden):not(.is-condition-hidden)').length;
|
||
var index = panel.getAttribute('data-document-field-panel');
|
||
var tab = document.querySelector('[data-document-field-tab="' + index + '"]');
|
||
if (tab) {
|
||
tab.hidden = count === 0;
|
||
var badge = tab.querySelector('.tab-count');
|
||
if (badge) { badge.textContent = count; }
|
||
}
|
||
if (count > 0 && firstVisible === null) { firstVisible = index; }
|
||
});
|
||
var active = document.querySelector('[data-document-field-tab].is-active:not([hidden])');
|
||
if (!active && firstVisible !== null) { this.activateFieldGroup(firstVisible); }
|
||
},
|
||
|
||
searchCatalog: function (catalog, value) {
|
||
if (!catalog) { return; }
|
||
value = String(value || '').trim().toLowerCase();
|
||
catalog.querySelectorAll('[data-document-catalog-node]').forEach(function (node) {
|
||
node.classList.toggle('is-filtered', value && String(node.getAttribute('data-search') || '').toLowerCase().indexOf(value) < 0);
|
||
});
|
||
},
|
||
|
||
submit: function (stay) {
|
||
if (!this.form || this.submitting) { return; }
|
||
var self = this;
|
||
var id = parseInt(this.field('id').value, 10) || 0;
|
||
this.clearErrors();
|
||
this.syncEditors();
|
||
this.reindexMediaLists();
|
||
this.reindexValueLists();
|
||
if (!this.form.checkValidity()) {
|
||
var nativeErrors = {};
|
||
this.form.querySelectorAll(':invalid').forEach(function (field) {
|
||
if (field.name && !nativeErrors[field.name]) { nativeErrors[field.name] = field.validationMessage || 'Проверьте значение'; }
|
||
});
|
||
this.showErrors(nativeErrors);
|
||
return;
|
||
}
|
||
this.checkAlias(false, function (ok) {
|
||
if (!ok) { return; }
|
||
self.checkShortAlias(false, function (shortOk) {
|
||
if (!shortOk) { return; }
|
||
self.setSaving(true);
|
||
var submitUrl = self.form.getAttribute('data-submit-url') || (self.base() + '/documents' + (id ? '/' + id : ''));
|
||
var returnUrl = self.form.getAttribute('data-return-url') || (self.base() + '/documents');
|
||
self.ajax(submitUrl, new FormData(self.form), function (payload) {
|
||
self.clearLocalDraft();
|
||
self.setDirty(false);
|
||
self.setSaving(false);
|
||
Adminx.Toast.show(payload.message || 'Документ сохранён', 'success');
|
||
if (payload.data && payload.data.snapshot_warning) {
|
||
Adminx.Toast.show('Документ сохранён, но JSON-снимок не записан: ' + payload.data.snapshot_warning, 'warning');
|
||
}
|
||
if (payload.data && payload.data.id) {
|
||
self.field('id').value = payload.data.id;
|
||
self.form.setAttribute('data-id', payload.data.id);
|
||
if (!id && self.form.getAttribute('data-submit-url')) {
|
||
self.form.setAttribute('data-submit-url', self.base() + '/documents/' + payload.data.id);
|
||
}
|
||
}
|
||
if (payload.data && payload.data.document_version) {
|
||
var versionField = self.field('document_version');
|
||
if (versionField) { versionField.value = String(payload.data.document_version); }
|
||
}
|
||
if (payload.data && payload.data.media_draft_token) {
|
||
self.refreshMediaDraft(payload.data.media_draft_token, payload.data.id || id);
|
||
}
|
||
if (self.form.getAttribute('data-quick-edit') === '1') {
|
||
self.refreshQuickEditOpener(!stay);
|
||
return;
|
||
}
|
||
if (!stay) {
|
||
window.location.href = returnUrl;
|
||
return;
|
||
}
|
||
if (payload.redirect && !id) {
|
||
var stayTemplate = self.form.getAttribute('data-stay-url-template');
|
||
var stayUrl = stayTemplate && payload.data && payload.data.id ? stayTemplate.replace('{id}', payload.data.id) : payload.redirect;
|
||
self.promoteCreatedDocument(payload.data.id, stayUrl);
|
||
}
|
||
}, function (payload) {
|
||
self.setSaving(false);
|
||
if (payload.data && payload.data.conflict) {
|
||
self.saveLocalDraft(true);
|
||
self.showEditConflict(payload);
|
||
return false;
|
||
}
|
||
self.showErrors(payload.errors || {});
|
||
});
|
||
});
|
||
});
|
||
},
|
||
|
||
setDirty: function (dirty) {
|
||
if (!this.form) { return; }
|
||
this.dirty = !!dirty;
|
||
if (this.dirty) { this.scheduleLocalDraft(); }
|
||
this.setSaveState(this.submitting ? 'saving' : (this.dirty ? 'dirty' : 'saved'));
|
||
},
|
||
|
||
initWorkspace: function () {
|
||
if (!this.form || !document.querySelector('[data-document-workspace]')) { return; }
|
||
var panel = 'main';
|
||
try { panel = window.localStorage.getItem(this.workspaceKey()) || panel; } catch (e) {}
|
||
this.setWorkspacePanel(panel, false);
|
||
},
|
||
|
||
workspaceKey: function () {
|
||
var actor = this.form ? this.form.getAttribute('data-actor-id') || '0' : '0';
|
||
var rubric = this.form ? this.form.getAttribute('data-rubric-id') || '0' : '0';
|
||
var workspace = document.querySelector('[data-document-workspace]');
|
||
var kind = workspace && workspace.getAttribute('data-catalog-mode') === '1' ? 'product' : 'document';
|
||
return 'ave.adminx.document-workspace:' + actor + ':' + rubric + ':' + kind;
|
||
},
|
||
|
||
setWorkspacePanel: function (panel, persist) {
|
||
var workspace = document.querySelector('[data-document-workspace]');
|
||
if (!workspace) { return; }
|
||
var button = workspace.querySelector('[data-document-workspace-tab="' + panel + '"]');
|
||
if (!button || button.getAttribute('aria-disabled') === 'true') {
|
||
panel = 'main';
|
||
button = workspace.querySelector('[data-document-workspace-tab="main"]');
|
||
}
|
||
this.workspacePanel = panel;
|
||
workspace.setAttribute('data-active-panel', panel);
|
||
workspace.querySelectorAll('[data-document-workspace-tab]').forEach(function (tab) {
|
||
var active = tab.getAttribute('data-document-workspace-tab') === panel;
|
||
tab.classList.toggle('is-active', active);
|
||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
tab.setAttribute('tabindex', active ? '0' : '-1');
|
||
});
|
||
var select = workspace.querySelector('[data-document-workspace-select]');
|
||
if (select) { select.value = panel; }
|
||
if (persist) {
|
||
try { window.localStorage.setItem(this.workspaceKey(), panel); } catch (e) {}
|
||
}
|
||
window.setTimeout(function () {
|
||
if (Adminx.CodeEditor) { Adminx.CodeEditor.refreshAll(); }
|
||
if (persist && button && typeof button.focus === 'function' && document.activeElement !== select) {
|
||
button.focus({ preventScroll: true });
|
||
}
|
||
}, 40);
|
||
},
|
||
|
||
workspaceForTarget: function (target) {
|
||
if (!target) { return 'main'; }
|
||
var productPanel = target.closest('[data-document-workspace-panel]');
|
||
if (productPanel) { return productPanel.getAttribute('data-document-workspace-panel') || 'main'; }
|
||
var section = target.closest('[data-document-section]');
|
||
return section ? section.getAttribute('data-document-section') || 'main' : 'main';
|
||
},
|
||
|
||
setWorkspaceForTarget: function (target) {
|
||
this.setWorkspacePanel(this.workspaceForTarget(target), true);
|
||
},
|
||
|
||
saveActiveWorkspace: function () {
|
||
var workspace = document.querySelector('[data-document-workspace]');
|
||
var panel = workspace ? workspace.getAttribute('data-active-panel') || 'main' : 'main';
|
||
if (panel === 'attributes' || panel === 'shipping' || panel === 'promotions') {
|
||
var form = workspace.querySelector('[data-document-workspace-panel="' + panel + '"] form');
|
||
if (form && typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||
else if (form) { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); }
|
||
return;
|
||
}
|
||
if (panel === 'variants') {
|
||
if (Adminx.Toast) { Adminx.Toast.show('В этом разделе нет несохранённых полей', 'info'); }
|
||
return;
|
||
}
|
||
this.submit(true);
|
||
},
|
||
|
||
draftKey: function () {
|
||
if (!this.form) { return ''; }
|
||
var actor = this.form.getAttribute('data-actor-id') || '0';
|
||
var id = parseInt((this.field('id') || {}).value, 10) || 0;
|
||
var rubric = this.form.getAttribute('data-rubric-id') || '0';
|
||
if (id > 0) { return 'ave.adminx.document-draft:' + actor + ':document-' + id; }
|
||
var instanceKey = 'ave.adminx.document-draft-instance:' + actor + ':' + rubric + ':' + window.location.pathname;
|
||
var instance = '';
|
||
try {
|
||
instance = window.sessionStorage.getItem(instanceKey) || '';
|
||
if (!instance) {
|
||
instance = Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
||
window.sessionStorage.setItem(instanceKey, instance);
|
||
}
|
||
} catch (e) { instance = 'default'; }
|
||
return 'ave.adminx.document-draft:' + actor + ':new-' + rubric + '-' + instance;
|
||
},
|
||
|
||
scheduleLocalDraft: function () {
|
||
var self = this;
|
||
window.clearTimeout(this.draftTimer);
|
||
this.draftTimer = window.setTimeout(function () { self.saveLocalDraft(false); }, 700);
|
||
},
|
||
|
||
formValues: function () {
|
||
if (!this.form) { return {}; }
|
||
this.syncEditors();
|
||
this.reindexMediaLists();
|
||
this.reindexValueLists();
|
||
var values = {};
|
||
var excluded = { _csrf: true, id: true, document_version: true, media_draft_token: true };
|
||
new FormData(this.form).forEach(function (value, name) {
|
||
if (excluded[name] || typeof value !== 'string') { return; }
|
||
if (!Object.prototype.hasOwnProperty.call(values, name)) { values[name] = []; }
|
||
values[name].push(value);
|
||
});
|
||
return values;
|
||
},
|
||
|
||
saveLocalDraft: function (immediate) {
|
||
if (!this.form || (!this.dirty && !immediate)) { return; }
|
||
window.clearTimeout(this.draftTimer);
|
||
var version = parseInt((this.field('document_version') || {}).value, 10) || 1;
|
||
var draft = {
|
||
format: 'ave.adminx.document-draft',
|
||
version: 1,
|
||
document_id: parseInt((this.field('id') || {}).value, 10) || 0,
|
||
rubric_id: parseInt(this.form.getAttribute('data-rubric-id'), 10) || 0,
|
||
server_version: version,
|
||
saved_at: Date.now(),
|
||
values: this.formValues()
|
||
};
|
||
try {
|
||
window.localStorage.setItem(this.draftKey(), JSON.stringify(draft));
|
||
this.setSaveState('draft');
|
||
} catch (e) {}
|
||
},
|
||
|
||
checkLocalDraft: function () {
|
||
if (!this.form) { return; }
|
||
var raw = '';
|
||
try { raw = window.localStorage.getItem(this.draftKey()) || ''; } catch (e) { return; }
|
||
if (!raw) { return; }
|
||
try { this.draftCandidate = JSON.parse(raw); } catch (e) { this.clearLocalDraft(); return; }
|
||
if (!this.draftCandidate || this.draftCandidate.format !== 'ave.adminx.document-draft' || !this.draftCandidate.values) { this.clearLocalDraft(); return; }
|
||
if (JSON.stringify(this.draftCandidate.values) === JSON.stringify(this.formValues())) { this.clearLocalDraft(); return; }
|
||
var root = this.form.querySelector('[data-document-draft-recovery]');
|
||
if (!root) { return; }
|
||
var when = new Date(this.draftCandidate.saved_at || Date.now());
|
||
var text = root.querySelector('[data-document-draft-recovery-text]');
|
||
var currentVersion = parseInt((this.field('document_version') || {}).value, 10) || 1;
|
||
var suffix = this.draftCandidate.server_version !== currentVersion ? ' Серверная версия документа уже изменилась.' : '';
|
||
if (text) { text.textContent = 'Сохранён в этом браузере ' + when.toLocaleString('ru-RU') + '.' + suffix; }
|
||
root.hidden = false;
|
||
},
|
||
|
||
restoreLocalDraft: function () {
|
||
if (!this.form || !this.draftCandidate) { return; }
|
||
var values = this.draftCandidate.values || {};
|
||
var self = this;
|
||
Object.keys(values).forEach(function (name) {
|
||
var controls = Array.prototype.slice.call(self.form.querySelectorAll('[name="' + self.escapeSelector(name) + '"]'));
|
||
var saved = Array.isArray(values[name]) ? values[name].map(String) : [String(values[name])];
|
||
controls.forEach(function (control, index) {
|
||
if (control.type === 'checkbox' || control.type === 'radio') { control.checked = saved.indexOf(String(control.value)) !== -1; return; }
|
||
if (control.tagName === 'SELECT' && control.multiple) {
|
||
Array.prototype.forEach.call(control.options, function (option) { option.selected = saved.indexOf(String(option.value)) !== -1; });
|
||
return;
|
||
}
|
||
control.value = saved[Math.min(index, saved.length - 1)] || '';
|
||
if (control._adminxCodeMirror) { control._adminxCodeMirror.setValue(control.value); }
|
||
if (control._adminxTiptap) { control._adminxTiptap.commands.setContent(control.value || '<p></p>'); }
|
||
});
|
||
});
|
||
this.form.querySelectorAll('[data-document-term-input]').forEach(function (field) {
|
||
var hidden = field.querySelector('[data-document-term-value]');
|
||
field._termItems = self.normalizeTerms(hidden ? hidden.value : '');
|
||
self.renderTermChips(field);
|
||
});
|
||
this.form.querySelectorAll('[data-document-media-url]').forEach(function (input) { self.updateMediaPreview(input); });
|
||
this.applyFieldConditions();
|
||
this.updateSeo();
|
||
this.setDirty(true);
|
||
var root = this.form.querySelector('[data-document-draft-recovery]');
|
||
if (root) { root.hidden = true; }
|
||
Adminx.Toast.show('Локальный черновик восстановлен. Проверьте данные и сохраните документ.', 'success');
|
||
},
|
||
|
||
discardLocalDraft: function () {
|
||
this.clearLocalDraft();
|
||
this.draftCandidate = null;
|
||
var root = this.form ? this.form.querySelector('[data-document-draft-recovery]') : null;
|
||
if (root) { root.hidden = true; }
|
||
},
|
||
|
||
clearLocalDraft: function () {
|
||
window.clearTimeout(this.draftTimer);
|
||
try { window.localStorage.removeItem(this.draftKey()); } catch (e) {}
|
||
},
|
||
|
||
escapeSelector: function (value) {
|
||
if (window.CSS && typeof window.CSS.escape === 'function') { return window.CSS.escape(String(value)); }
|
||
return String(value).replace(/(["\\])/g, '\\$1');
|
||
},
|
||
|
||
promoteCreatedDocument: function (id, stayUrl) {
|
||
id = parseInt(id, 10) || 0;
|
||
if (!id) { return; }
|
||
var workspace = document.querySelector('[data-document-workspace]');
|
||
if (workspace && workspace.getAttribute('data-catalog-mode') === '1') {
|
||
window.location.href = stayUrl;
|
||
return;
|
||
}
|
||
window.history.replaceState({ adminxDocumentEdit: true }, '', stayUrl);
|
||
document.querySelectorAll('[data-document-remarks], [data-document-aliases], [data-document-revisions], [data-document-snapshot], [data-open-drawer="documentPresetDrawer"]').forEach(function (button) {
|
||
button.disabled = false;
|
||
});
|
||
var presetForm = document.querySelector('[data-document-preset-form]');
|
||
if (presetForm) { presetForm.setAttribute('action', this.base() + '/documents/' + id + '/presets'); }
|
||
document.querySelectorAll('[data-document-id]').forEach(function (element) {
|
||
element.setAttribute('data-document-id', String(id));
|
||
});
|
||
var note = document.querySelector('[data-document-new-note]');
|
||
if (note) { note.remove(); }
|
||
var factId = document.querySelector('[data-document-fact-id]');
|
||
if (factId) { factId.textContent = String(id); }
|
||
var titleField = this.field('document_title');
|
||
var heading = document.querySelector('[data-document-edit-heading]');
|
||
var title = titleField ? titleField.value.trim() : '';
|
||
if (heading && title) { heading.textContent = title; }
|
||
var status = this.field('document_status');
|
||
var meta = document.querySelector('[data-document-edit-meta]');
|
||
if (meta) { meta.textContent = 'ID ' + id + ', ' + (status && status.value === '1' ? 'опубликован' : 'черновик') + ', сохранён только что'; }
|
||
if (title) { document.title = title + ' · AVE.cms'; }
|
||
},
|
||
|
||
setSaving: function (saving) {
|
||
if (!this.form) { return; }
|
||
this.submitting = !!saving;
|
||
this.form.setAttribute('aria-busy', this.submitting ? 'true' : 'false');
|
||
this.form.querySelectorAll('[data-document-submit-stay], button[type="submit"]').forEach(function (button) {
|
||
button.disabled = !!saving;
|
||
});
|
||
this.setSaveState(this.submitting ? 'saving' : (this.dirty ? 'dirty' : 'saved'));
|
||
},
|
||
|
||
setSaveState: function (state) {
|
||
var root = document.querySelector('[data-document-save-state]');
|
||
if (!root) { return; }
|
||
var states = {
|
||
saved: { text: 'Изменения сохранены', icon: 'ti ti-circle-check' },
|
||
dirty: { text: 'Есть несохранённые изменения', icon: 'ti ti-point-filled' },
|
||
saving: { text: 'Сохраняем...', icon: 'ti ti-loader-2' },
|
||
draft: { text: 'Черновик сохранён в браузере', icon: 'ti ti-device-floppy' }
|
||
};
|
||
var current = states[state] || states.saved;
|
||
root.setAttribute('data-state', state);
|
||
var text = root.querySelector('[data-document-save-state-text]');
|
||
var icon = root.querySelector('[data-document-save-state-icon]');
|
||
if (text) { text.textContent = current.text; }
|
||
if (icon) { icon.className = current.icon; }
|
||
},
|
||
|
||
updateBoolean: function (input) {
|
||
if (!input) { return; }
|
||
var label = input.closest('.switch');
|
||
label = label ? label.querySelector('[data-document-boolean-label]') : null;
|
||
if (label) { label.textContent = input.checked ? 'Включено' : 'Выключено'; }
|
||
},
|
||
|
||
refreshQuickEditOpener: function (closeAfter) {
|
||
if (window.opener && !window.opener.closed) {
|
||
try {
|
||
window.opener.location.reload();
|
||
} catch (e) {
|
||
window.opener.postMessage({ type: 'adminx:document-saved' }, window.location.origin);
|
||
}
|
||
}
|
||
if (closeAfter) {
|
||
window.close();
|
||
}
|
||
},
|
||
|
||
previewPayload: function () {
|
||
if (!this.form) { return; }
|
||
this.syncEditors();
|
||
this.reindexMediaLists();
|
||
this.reindexValueLists();
|
||
var code = document.querySelector('[data-document-payload-code]');
|
||
var summary = document.querySelector('[data-document-payload-summary]');
|
||
var validation = document.querySelector('[data-document-payload-validation]');
|
||
if (code) { code.textContent = 'Формируем данные...'; }
|
||
if (validation) { validation.hidden = true; validation.innerHTML = ''; }
|
||
if (Adminx.Drawer) { Adminx.Drawer.open('documentPayloadDrawer'); }
|
||
var self = this;
|
||
this.ajax(this.base() + '/documents/preview', new FormData(this.form), function (payload) {
|
||
var data = payload.data || {};
|
||
var preview = data.payload || {};
|
||
if (code) { code.textContent = JSON.stringify(preview, null, 2); }
|
||
var fields = Array.isArray(preview.fields) ? preview.fields.length : 0;
|
||
if (summary) { summary.textContent = (preview.mode === 'create' ? 'Создание' : 'Обновление') + ' · полей: ' + fields + ' · без сохранения'; }
|
||
self.renderPayloadValidation(data.validation_errors || {});
|
||
});
|
||
},
|
||
|
||
renderPayloadValidation: function (errors) {
|
||
var target = document.querySelector('[data-document-payload-validation]');
|
||
if (!target) { return; }
|
||
var keys = Object.keys(errors || {});
|
||
if (!keys.length) { target.hidden = true; target.innerHTML = ''; return; }
|
||
target.innerHTML = '<div class="alert alert-warning"><i class="ti ti-alert-triangle alert-ic"></i><div><b class="alert-title">Валидация: ' + keys.length + '</b><span>' + keys.map(function (key) { return selfEsc(key + ': ' + errors[key]); }).join('<br>') + '</span></div></div>';
|
||
target.hidden = false;
|
||
function selfEsc(value) {
|
||
var node = document.createElement('span');
|
||
node.textContent = String(value || '');
|
||
return node.innerHTML;
|
||
}
|
||
},
|
||
|
||
copyPayload: function () {
|
||
var code = document.querySelector('[data-document-payload-code]');
|
||
var value = code ? code.textContent : '';
|
||
if (!value) { return; }
|
||
var done = function () { Adminx.Toast.show('JSON скопирован', 'success'); };
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(value).then(done);
|
||
return;
|
||
}
|
||
var textarea = document.createElement('textarea');
|
||
textarea.value = value;
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
textarea.remove();
|
||
done();
|
||
},
|
||
|
||
syncEditors: function () {
|
||
if (!this.form) { return; }
|
||
this.form.querySelectorAll('textarea[data-code-editor]').forEach(function (textarea) {
|
||
if (textarea._adminxCodeMirror) { textarea._adminxCodeMirror.save(); }
|
||
});
|
||
this.form.querySelectorAll('textarea[data-rich-editor]').forEach(function (textarea) {
|
||
if (textarea._adminxTiptap && textarea.getAttribute('data-rich-editor-dirty') === '1') {
|
||
textarea.value = textarea._adminxTiptap.getHTML();
|
||
}
|
||
});
|
||
},
|
||
|
||
remove: function (row) {
|
||
var self = this;
|
||
if (!row) { return; }
|
||
if (Adminx.Confirm) {
|
||
Adminx.Confirm.open({
|
||
kind: 'error',
|
||
title: 'Удалить документ?',
|
||
message: 'Документ будет помечен удалённым и останется доступен для восстановления.',
|
||
confirmLabel: 'Удалить',
|
||
confirmClass: 'btn-danger',
|
||
onConfirm: function () { self.action(row, '/delete'); }
|
||
});
|
||
return;
|
||
}
|
||
if (confirm('Пометить документ удалённым?')) { this.action(row, '/delete'); }
|
||
},
|
||
|
||
restore: function (row) {
|
||
if (!row) { return; }
|
||
this.action(row, '/restore');
|
||
},
|
||
|
||
copy: function (row) {
|
||
if (!row) { return; }
|
||
var self = this;
|
||
Adminx.Confirm.open({
|
||
kind: 'info',
|
||
title: 'Создать копию документа?',
|
||
message: 'Будет создан новый черновик со всеми значениями полей.',
|
||
confirmLabel: 'Создать копию',
|
||
onConfirm: function () {
|
||
var id = row.getAttribute('data-id');
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/' + encodeURIComponent(id) + '/copy', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Копия создана', 'success');
|
||
window.location.href = payload.redirect || (self.base() + '/documents/' + ((payload.data || {}).id || '') + '/edit');
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
purge: function (row) {
|
||
if (!row) { return; }
|
||
var self = this;
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Удалить документ окончательно?',
|
||
message: 'Будут удалены документ, значения полей, ревизии, редиректы, заметки и статистика. Действие необратимо.',
|
||
confirmLabel: 'Удалить окончательно',
|
||
onConfirm: function () { self.action(row, '/purge'); }
|
||
});
|
||
},
|
||
|
||
action: function (row, suffix) {
|
||
var id = row.getAttribute('data-id');
|
||
var data = new FormData();
|
||
data.append('_csrf', this.csrf());
|
||
var self = this;
|
||
this.ajax(this.base() + '/documents/' + encodeURIComponent(id) + suffix, data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Готово', 'success');
|
||
self.applyFilterUrl(window.location.href, false);
|
||
});
|
||
},
|
||
|
||
updateBulk: function () {
|
||
var selected = document.querySelectorAll('[data-document-check]:checked');
|
||
var bar = document.querySelector('[data-documents-bulk]');
|
||
var count = document.querySelector('[data-documents-selected]');
|
||
var all = document.querySelector('[data-documents-check-all]');
|
||
var total = document.querySelectorAll('[data-document-check]').length;
|
||
if (bar) {
|
||
bar.hidden = selected.length === 0;
|
||
bar.classList.toggle('visible', selected.length > 0);
|
||
}
|
||
if (count) { count.textContent = selected.length; }
|
||
if (all) { all.checked = total > 0 && selected.length === total; all.indeterminate = selected.length > 0 && selected.length < total; }
|
||
},
|
||
|
||
clearBulk: function () {
|
||
document.querySelectorAll('[data-document-check], [data-documents-check-all]').forEach(function (box) { box.checked = false; box.indeterminate = false; });
|
||
this.updateBulk();
|
||
},
|
||
|
||
applyBulk: function () {
|
||
var action = document.querySelector('[data-documents-bulk-action]');
|
||
var ids = Array.prototype.map.call(document.querySelectorAll('[data-document-check]:checked'), function (box) { return box.value; });
|
||
if (!action || !action.value || !ids.length) { Adminx.Toast.show('Выберите документы и действие', 'warning'); return; }
|
||
var self = this;
|
||
var labels = {publish:'опубликовать',unpublish:'снять с публикации',delete:'переместить в корзину',restore:'восстановить',purge:'удалить окончательно'};
|
||
Adminx.Confirm.open({kind:action.value==='purge'?'danger':'warning',title:'Выполнить пакетное действие?',message:'Документов: '+ids.length+'. Действие: '+labels[action.value]+'.',confirmLabel:'Применить',onConfirm:function(){var data=new FormData();data.append('_csrf',self.csrf());data.append('action',action.value);ids.forEach(function(id){data.append('ids[]',id);});self.ajax(self.base()+'/documents/bulk',data,function(payload){var result=payload.data||{};Adminx.Toast.show((payload.message||'Готово')+' · обработано '+(result.done||0),result.errors&&result.errors.length?'warning':'success');self.applyFilterUrl(window.location.href,false);});}});
|
||
},
|
||
|
||
rebuildSnapshots: function () {
|
||
var self = this;
|
||
var rubric = document.querySelector('.documents-filter [name="rubric_id"]');
|
||
var rubricId = rubric ? (parseInt(rubric.value, 10) || 0) : 0;
|
||
var scope = rubricId ? ('выбранной рубрики #' + rubricId) : 'всех рабочих документов';
|
||
Adminx.Confirm.open({
|
||
kind: 'warning',
|
||
title: 'Пересобрать JSON-снимки?',
|
||
message: 'Снимки будут последовательно пересобраны для ' + scope + '. Окно нужно оставить открытым до завершения.',
|
||
confirmLabel: 'Пересобрать',
|
||
onConfirm: function () { self.runSnapshotBatch(rubricId, 0, 0, []); }
|
||
});
|
||
},
|
||
|
||
runSnapshotBatch: function (rubricId, after, total, failures) {
|
||
var self = this;
|
||
var data = new FormData();
|
||
data.append('_csrf', this.csrf());
|
||
data.append('rubric_id', rubricId || 0);
|
||
data.append('after', after || 0);
|
||
data.append('limit', 50);
|
||
Adminx.Loader.show();
|
||
fetch(this.base() + '/documents/snapshots/rebuild', { method: 'POST', body: data, headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
if (!payload.success) { throw payload; }
|
||
var result = payload.data || {};
|
||
total += parseInt(result.done, 10) || 0;
|
||
failures = failures.concat(result.failed || []);
|
||
if (result.has_more) {
|
||
self.runSnapshotBatch(rubricId, parseInt(result.next_after, 10) || after, total, failures);
|
||
return;
|
||
}
|
||
Adminx.Loader.hide();
|
||
Adminx.Toast.show('JSON-снимки пересобраны: ' + total + (failures.length ? ', ошибок: ' + failures.length : ''), failures.length ? 'warning' : 'success');
|
||
})
|
||
.catch(function (payload) {
|
||
Adminx.Loader.hide();
|
||
Adminx.Toast.show((payload && payload.message) || 'Пакетная пересборка остановлена', 'error');
|
||
});
|
||
},
|
||
|
||
openSnapshot: function () {
|
||
if (!this.form) { return; }
|
||
var idField = this.field('id');
|
||
var id = idField ? (parseInt(idField.value, 10) || 0) : 0;
|
||
if (!id) { return; }
|
||
if (Adminx.Drawer) { Adminx.Drawer.open('documentSnapshotDrawer'); }
|
||
this.loadSnapshot(id);
|
||
},
|
||
|
||
loadSnapshot: function (id) {
|
||
var self = this;
|
||
var target = document.querySelector('[data-document-snapshot-status]');
|
||
if (target) { target.innerHTML = '<div><dt>Состояние</dt><dd>Загрузка...</dd></div>'; }
|
||
fetch(this.base() + '/documents/' + id + '/snapshot', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) { self.renderSnapshot(payload.data || {}); })
|
||
.catch(function (payload) { Adminx.Toast.show((payload && payload.message) || 'Не удалось получить статус снимка', 'error'); });
|
||
},
|
||
|
||
renderSnapshot: function (status) {
|
||
var target = document.querySelector('[data-document-snapshot-status]');
|
||
if (!target) { return; }
|
||
var state = status.current ? '<span class="badge badge-green">актуален</span>' : (status.exists ? '<span class="badge badge-amber">нужна пересборка</span>' : '<span class="badge badge-gray">не создан</span>');
|
||
target.innerHTML = '<div><dt>Состояние</dt><dd>' + state + '</dd></div>'
|
||
+ '<div><dt>Сформирован</dt><dd>' + this.esc(status.generated_label || 'ещё не создан') + '</dd></div>'
|
||
+ '<div><dt>Поля</dt><dd class="mono">' + this.esc(status.fields_count || 0) + '</dd></div>'
|
||
+ '<div><dt>Размер</dt><dd>' + this.esc(status.size_label || '0 Б') + '</dd></div>'
|
||
+ '<div><dt>Файл</dt><dd class="mono">' + this.esc(status.path || '') + '</dd></div>';
|
||
},
|
||
|
||
rebuildSnapshot: function () {
|
||
if (!this.form) { return; }
|
||
var idField = this.field('id');
|
||
var id = idField ? (parseInt(idField.value, 10) || 0) : 0;
|
||
if (!id) { return; }
|
||
var data = new FormData();
|
||
data.append('_csrf', this.csrf());
|
||
var self = this;
|
||
this.ajax(this.base() + '/documents/' + id + '/snapshot/rebuild', data, function (payload) {
|
||
self.renderSnapshot(payload.data || {});
|
||
Adminx.Toast.show(payload.message || 'JSON-снимок пересобран', 'success');
|
||
});
|
||
},
|
||
|
||
ajax: function (url, data, done, fail) {
|
||
Adminx.Loader.show();
|
||
fetch(url, { method: 'POST', body: data, headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
if (!payload.success) { throw payload; }
|
||
if (done) { done(payload); }
|
||
})
|
||
.catch(function (payload) {
|
||
var handled = fail ? fail(payload || {}) === false : false;
|
||
if (handled) { return; }
|
||
Adminx.Toast.show((payload && payload.message) || 'Ошибка запроса', 'error');
|
||
})
|
||
.finally(function () { Adminx.Loader.hide(); });
|
||
},
|
||
|
||
showEditConflict: function (payload) {
|
||
var data = payload.data || {};
|
||
var reload = function () { window.location.href = data.reload_url || window.location.href; };
|
||
if (window.Adminx && Adminx.Confirm && typeof Adminx.Confirm.open === 'function') {
|
||
Adminx.Confirm.open({
|
||
kind: 'warning',
|
||
title: 'Документ изменился',
|
||
message: 'На сервере уже сохранена более новая версия. Обновите страницу, проверьте изменения и сохраните документ повторно.',
|
||
confirmLabel: 'Обновить страницу',
|
||
cancelLabel: 'Остаться',
|
||
onConfirm: reload
|
||
});
|
||
return;
|
||
}
|
||
Adminx.Toast.show(payload.message || 'Документ уже изменён. Обновите страницу.', 'warning');
|
||
},
|
||
|
||
json: function (res) {
|
||
return res.text().then(function (raw) {
|
||
var payload = {};
|
||
try {
|
||
payload = raw ? JSON.parse(raw) : {};
|
||
} catch (e) {
|
||
payload = {
|
||
success: false,
|
||
message: res.status >= 500
|
||
? 'Внутренняя ошибка сервера. Обновите страницу и повторите попытку.'
|
||
: 'Сервер вернул некорректный ответ.'
|
||
};
|
||
}
|
||
if (!res.ok || payload.success === false) { throw payload; }
|
||
return payload;
|
||
});
|
||
},
|
||
|
||
csrf: function () {
|
||
var field = this.form ? this.field('_csrf') : document.querySelector('[data-documents-csrf]');
|
||
return field ? field.value : '';
|
||
},
|
||
|
||
issueApiToken: function (form) {
|
||
if (!form || form.getAttribute('aria-busy') === 'true') { return; }
|
||
var scopes = form.querySelectorAll('[name="scopes[]"]:checked');
|
||
if (!scopes.length) {
|
||
Adminx.Toast.show('Выберите хотя бы одно разрешение', 'warning');
|
||
return;
|
||
}
|
||
var self = this;
|
||
var submit = form.querySelector('[data-api-token-submit]');
|
||
form.setAttribute('aria-busy', 'true');
|
||
if (submit) { submit.disabled = true; }
|
||
var data = new FormData(form);
|
||
data.append('_csrf', this.csrf());
|
||
this.ajax((form.getAttribute('data-base') || this.base()) + '/documents/api/tokens', data, function (payload) {
|
||
var secret = form.querySelector('[data-api-token-secret]');
|
||
var fields = form.querySelector('[data-api-token-fields]');
|
||
var value = form.querySelector('[data-api-token-value]');
|
||
var subtitle = document.querySelector('[data-api-token-subtitle]');
|
||
if (value) { value.value = payload.data && payload.data.token ? payload.data.token : ''; }
|
||
if (fields) { fields.hidden = true; }
|
||
if (secret) { secret.hidden = false; }
|
||
if (submit) { submit.hidden = true; }
|
||
if (subtitle) { subtitle.textContent = 'Секрет готов к копированию.'; }
|
||
form.removeAttribute('aria-busy');
|
||
form.closest('.drawer').setAttribute('data-token-created', '1');
|
||
Adminx.Toast.show(payload.message || 'API-токен создан', 'success');
|
||
window.setTimeout(function () { if (value) { value.focus(); value.select(); } }, 30);
|
||
}, function () {
|
||
form.removeAttribute('aria-busy');
|
||
if (submit) { submit.disabled = false; }
|
||
});
|
||
},
|
||
|
||
copyApiToken: function () {
|
||
var value = document.querySelector('[data-api-token-value]');
|
||
var token = value ? value.value : '';
|
||
if (!token) { return; }
|
||
var label = document.querySelector('[data-api-token-copy-label]');
|
||
var done = function () {
|
||
if (label) { label.textContent = 'Скопировано'; }
|
||
Adminx.Toast.show('Токен скопирован', 'success');
|
||
window.setTimeout(function () { if (label) { label.textContent = 'Копировать'; } }, 1600);
|
||
};
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(token).then(done).catch(function () {
|
||
value.focus(); value.select(); document.execCommand('copy'); done();
|
||
});
|
||
return;
|
||
}
|
||
value.focus(); value.select(); document.execCommand('copy'); done();
|
||
},
|
||
|
||
revokeApiToken: function (row) {
|
||
if (!row) { return; }
|
||
var self = this;
|
||
var id = parseInt(row.getAttribute('data-id'), 10) || 0;
|
||
if (!id) { return; }
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Отозвать API-токен?',
|
||
message: 'Интеграция сразу потеряет доступ. Вернуть этот токен будет невозможно.',
|
||
confirmLabel: 'Отозвать',
|
||
onConfirm: function () {
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/api/tokens/' + id + '/revoke', data, function (payload) {
|
||
var state = row.querySelector('[data-api-token-state]');
|
||
var button = row.querySelector('[data-api-token-revoke]');
|
||
if (state) { state.className = 'badge badge-gray'; state.textContent = 'Отозван'; }
|
||
if (button) { button.remove(); }
|
||
row.classList.add('is-revoked');
|
||
Adminx.Toast.show(payload.message || 'API-токен отозван', 'success');
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
resetApiTokenForm: function () {
|
||
var form = document.querySelector('[data-api-token-form]');
|
||
if (!form) { return; }
|
||
form.reset();
|
||
form.removeAttribute('aria-busy');
|
||
var fields = form.querySelector('[data-api-token-fields]');
|
||
var secret = form.querySelector('[data-api-token-secret]');
|
||
var value = form.querySelector('[data-api-token-value]');
|
||
var submit = form.querySelector('[data-api-token-submit]');
|
||
var subtitle = document.querySelector('[data-api-token-subtitle]');
|
||
if (fields) { fields.hidden = false; }
|
||
if (secret) { secret.hidden = true; }
|
||
if (value) { value.value = ''; }
|
||
if (submit) { submit.hidden = false; submit.disabled = false; }
|
||
if (subtitle) { subtitle.textContent = 'Права доступа для одной внешней интеграции.'; }
|
||
},
|
||
|
||
clearErrors: function () {
|
||
if (!this.form) { return; }
|
||
this.form.querySelectorAll('.field-error').forEach(function (el) { el.textContent = ''; });
|
||
this.form.querySelectorAll('.is-invalid').forEach(function (el) { el.classList.remove('is-invalid'); });
|
||
this.errorTargets = [];
|
||
var summary = this.form.querySelector('[data-document-error-summary]');
|
||
var list = summary ? summary.querySelector('[data-document-error-list]') : null;
|
||
if (list) { list.innerHTML = ''; }
|
||
if (summary) { summary.hidden = true; }
|
||
},
|
||
|
||
showErrors: function (errors) {
|
||
var self = this;
|
||
var jumpPanel = null;
|
||
var summaryItems = [];
|
||
Object.keys(errors || {}).forEach(function (key) {
|
||
var fieldMatch = key.match(/^fields(?:\[(\d+)\]|\.(\d+))$/);
|
||
var target = null;
|
||
var label = key;
|
||
if (fieldMatch) {
|
||
var fieldId = fieldMatch[1] || fieldMatch[2];
|
||
var container = self.form.querySelector('.ax-document-field[data-rubric-field-id="' + fieldId + '"]');
|
||
if (container) {
|
||
container.classList.add('is-invalid');
|
||
target = container;
|
||
var fieldLabel = container.querySelector('.ax-label');
|
||
if (fieldLabel) { label = fieldLabel.textContent.replace('*', '').trim(); }
|
||
var err = container.querySelector('[data-field-value-error]');
|
||
if (!err) {
|
||
err = document.createElement('div');
|
||
err.className = 'field-error';
|
||
err.setAttribute('data-field-value-error', '');
|
||
(container.querySelector('.ax-document-field-body') || container).appendChild(err);
|
||
}
|
||
err.textContent = errors[key];
|
||
var panel = container.closest('[data-document-field-panel]');
|
||
if (panel && jumpPanel === null) { jumpPanel = panel.getAttribute('data-document-field-panel'); }
|
||
}
|
||
if (target) { summaryItems.push({ key: key, label: label, message: String(errors[key]), target: target }); }
|
||
return;
|
||
}
|
||
var error = self.form.querySelector('[data-error="' + key + '"]');
|
||
var field = self.field(key);
|
||
if (error) { error.textContent = errors[key]; }
|
||
if (field) { field.classList.add('is-invalid'); }
|
||
target = field || error;
|
||
if (field) {
|
||
var fieldRoot = field.closest('.field');
|
||
var directLabel = fieldRoot ? fieldRoot.querySelector('.field-label') : null;
|
||
if (directLabel) { label = directLabel.textContent.replace(/\d+\s*\/\s*\d+$/, '').trim(); }
|
||
}
|
||
if (target) { summaryItems.push({ key: key, label: label, message: String(errors[key]), target: target }); }
|
||
});
|
||
this.renderErrorSummary(summaryItems);
|
||
// если ошибка в поле скрытой группы — переключаемся на её вкладку
|
||
if (jumpPanel !== null) { self.activateFieldGroup(jumpPanel); }
|
||
var firstInvalid = self.form.querySelector('.is-invalid');
|
||
if (firstInvalid) {
|
||
self.setWorkspaceForTarget(firstInvalid);
|
||
window.setTimeout(function () {
|
||
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
var input = firstInvalid.matches('input,select,textarea') ? firstInvalid : firstInvalid.querySelector('input,select,textarea,[contenteditable="true"]');
|
||
if (input && typeof input.focus === 'function') { input.focus({ preventScroll: true }); }
|
||
}, 80);
|
||
}
|
||
},
|
||
|
||
renderErrorSummary: function (items) {
|
||
var summary = this.form ? this.form.querySelector('[data-document-error-summary]') : null;
|
||
var list = summary ? summary.querySelector('[data-document-error-list]') : null;
|
||
var title = summary ? summary.querySelector('[data-document-error-title]') : null;
|
||
if (!summary || !list || !items.length) { return; }
|
||
this.errorTargets = items.map(function (item) { return item.target; });
|
||
list.innerHTML = '';
|
||
items.forEach(function (item, index) {
|
||
var button = document.createElement('button');
|
||
button.type = 'button';
|
||
button.className = 'documents-error-jump';
|
||
button.setAttribute('data-document-error-jump', String(index));
|
||
var name = document.createElement('b');
|
||
name.textContent = item.label;
|
||
var message = document.createElement('span');
|
||
message.textContent = item.message;
|
||
var icon = document.createElement('i');
|
||
icon.className = 'ti ti-arrow-up-right';
|
||
button.appendChild(name);
|
||
button.appendChild(message);
|
||
button.appendChild(icon);
|
||
list.appendChild(button);
|
||
});
|
||
if (title) { title.textContent = 'Найдено ошибок: ' + items.length; }
|
||
summary.hidden = false;
|
||
summary.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
},
|
||
|
||
jumpToError: function (index) {
|
||
var target = this.errorTargets[parseInt(index, 10) || 0];
|
||
if (!target) { return; }
|
||
this.setWorkspaceForTarget(target);
|
||
var panel = target.closest('[data-document-field-panel]');
|
||
if (panel) { this.activateFieldGroup(panel.getAttribute('data-document-field-panel')); }
|
||
window.setTimeout(function () {
|
||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
var input = target.matches('input,select,textarea') ? target : target.querySelector('input,select,textarea,[contenteditable="true"]');
|
||
if (input && typeof input.focus === 'function') { input.focus({ preventScroll: true }); }
|
||
}, 90);
|
||
},
|
||
|
||
scheduleAliasCheck: function () {
|
||
var self = this;
|
||
clearTimeout(this.aliasTimer);
|
||
this.setAliasState('', 'Проверка...');
|
||
this.aliasTimer = setTimeout(function () { self.checkAlias(false); }, 350);
|
||
},
|
||
|
||
checkAlias: function (required, done) {
|
||
if (!this.form) { if (done) { done(true); } return; }
|
||
var alias = this.field('document_alias').value.trim();
|
||
var id = this.field('id').value || 0;
|
||
var self = this;
|
||
if (!alias) {
|
||
this.setAliasState('empty', 'Alias можно оставить пустым');
|
||
if (done) { done(true); }
|
||
return;
|
||
}
|
||
fetch(this.base() + '/documents/alias-check?alias=' + encodeURIComponent(alias) + '&id=' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
var data = payload.data || {};
|
||
self.setAliasState(data.state || (data.ok ? 'ok' : 'error'), data.message || payload.message || '');
|
||
if (done) { done(data.ok !== false); }
|
||
})
|
||
.catch(function () {
|
||
self.setAliasState('error', 'Не удалось проверить alias');
|
||
if (done) { done(!required); }
|
||
});
|
||
},
|
||
|
||
setAliasState: function (state, message) {
|
||
var el = document.querySelector('[data-document-alias-state]');
|
||
this.aliasState = state || '';
|
||
if (!el) { return; }
|
||
el.textContent = message || '';
|
||
el.classList.toggle('is-ok', state === 'ok');
|
||
el.classList.toggle('is-error', state === 'error');
|
||
},
|
||
|
||
scheduleShortAliasCheck: function () {
|
||
var self = this;
|
||
clearTimeout(this.shortAliasTimer);
|
||
this.setShortAliasState('', 'Проверка...');
|
||
this.shortAliasTimer = setTimeout(function () { self.checkShortAlias(false); }, 350);
|
||
},
|
||
|
||
checkShortAlias: function (required, done) {
|
||
if (!this.form) { if (done) { done(true); } return; }
|
||
var input = this.field('document_short_alias');
|
||
var alias = input ? input.value.trim() : '';
|
||
var id = this.field('id').value || 0;
|
||
var self = this;
|
||
if (!alias) {
|
||
this.setShortAliasState('empty', 'До 10 символов, необязательно.');
|
||
if (done) { done(true); }
|
||
return;
|
||
}
|
||
|
||
fetch(this.base() + '/documents/alias-check?kind=short&alias=' + encodeURIComponent(alias) + '&id=' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
var data = payload.data || {};
|
||
self.setShortAliasState(data.state || (data.ok ? 'ok' : 'error'), data.message || payload.message || '');
|
||
if (done) { done(data.ok !== false); }
|
||
})
|
||
.catch(function () {
|
||
self.setShortAliasState('error', 'Не удалось проверить короткий alias');
|
||
if (done) { done(!required); }
|
||
});
|
||
},
|
||
|
||
setShortAliasState: function (state, message) {
|
||
var el = document.querySelector('[data-document-short-alias-state]');
|
||
if (!el) { return; }
|
||
el.textContent = message || '';
|
||
el.classList.toggle('is-ok', state === 'ok');
|
||
el.classList.toggle('is-error', state === 'error');
|
||
},
|
||
|
||
slugify: function (value) {
|
||
var map = {
|
||
'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'e','ж':'zh','з':'z','и':'i',
|
||
'й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r','с':'s','т':'t',
|
||
'у':'u','ф':'f','х':'h','ц':'c','ч':'ch','ш':'sh','щ':'sch','ъ':'','ы':'y','ь':'',
|
||
'э':'e','ю':'yu','я':'ya'
|
||
};
|
||
return String(value || '').toLowerCase().replace(/[а-яё]/g, function (ch) { return map[ch] !== undefined ? map[ch] : ch; })
|
||
.replace(/[^a-z0-9]+/g, '-').replace(/-{2,}/g, '-').replace(/^-+|-+$/g, '');
|
||
},
|
||
|
||
rubricAliasTemplate: function () {
|
||
var select = this.field('rubric_id');
|
||
if (!select || !select.options || select.selectedIndex < 0) { return ''; }
|
||
return String(select.options[select.selectedIndex].getAttribute('data-alias-template') || '').replace(/^\/+|\/+$/g, '');
|
||
},
|
||
|
||
expandAliasTemplate: function (pattern) {
|
||
var published = this.field('document_published');
|
||
var value = published && published.value ? new Date(published.value) : new Date();
|
||
if (isNaN(value.getTime())) { value = new Date(); }
|
||
var pad = function (part) { return String(part).padStart(2, '0'); };
|
||
var replacements = {
|
||
'%d': pad(value.getDate()), '%m': pad(value.getMonth() + 1), '%Y': String(value.getFullYear()),
|
||
'%y': String(value.getFullYear()).slice(-2), '%H': pad(value.getHours()), '%M': pad(value.getMinutes()), '%S': pad(value.getSeconds())
|
||
};
|
||
pattern = String(pattern || '');
|
||
Object.keys(replacements).forEach(function (token) { pattern = pattern.split(token).join(replacements[token]); });
|
||
return pattern.replace(/^\/+|\/+$/g, '');
|
||
},
|
||
|
||
composeRubricAlias: function (leaf) {
|
||
leaf = String(leaf || '').replace(/^\/+|\/+$/g, '');
|
||
var prefix = this.expandAliasTemplate(this.rubricAliasTemplate());
|
||
return prefix ? prefix + '/' + leaf : leaf;
|
||
},
|
||
|
||
updateAliasTemplateHint: function () {
|
||
var hint = document.querySelector('[data-document-alias-template-hint]');
|
||
if (!hint) { return; }
|
||
var pattern = this.rubricAliasTemplate();
|
||
hint.textContent = pattern
|
||
? 'Шаблон рубрики: /' + pattern + '/ + alias документа. Дата берётся из публикации.'
|
||
: 'У рубрики нет шаблона пути: alias документа используется от корня сайта.';
|
||
},
|
||
|
||
syncSlugFromTitle: function () {
|
||
if (this.aliasTouched) { return; }
|
||
var title = this.field('document_title');
|
||
var alias = this.field('document_alias');
|
||
if (!title || !alias) { return; }
|
||
var leaf = this.slugify(title.value);
|
||
alias.value = this.composeRubricAlias(leaf);
|
||
this.updateAliasTemplateHint();
|
||
this.scheduleAliasCheck();
|
||
},
|
||
|
||
generateSlug: function (button) {
|
||
var self = this;
|
||
var title = this.field('document_title');
|
||
var alias = this.field('document_alias');
|
||
if (!title || !alias) { return; }
|
||
var parent = this.field('document_parent');
|
||
var body = new FormData();
|
||
body.append('title', title.value || '');
|
||
body.append('id', button.getAttribute('data-document-id') || '0');
|
||
body.append('parent', parent ? (parent.value || '0') : '0');
|
||
var rubric = this.field('rubric_id');
|
||
var published = this.field('document_published');
|
||
body.append('rubric_id', rubric ? (rubric.value || '0') : '0');
|
||
body.append('published', published ? (published.value || '') : '');
|
||
body.append('_csrf', this.csrf());
|
||
fetch(button.getAttribute('data-slug-generate'), {
|
||
method: 'POST', body: body,
|
||
headers: { 'Accept': 'application/json', 'X-CSRF-Token': this.csrf() },
|
||
credentials: 'same-origin'
|
||
})
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
var data = payload.data || {};
|
||
if (data.alias) {
|
||
alias.value = data.alias;
|
||
self.aliasTouched = true;
|
||
self.checkAlias(false);
|
||
self.updateSeo();
|
||
} else if (payload.message && window.Adminx && Adminx.toast) {
|
||
Adminx.toast(payload.message, 'error');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
if (window.Adminx && Adminx.toast) { Adminx.toast('Не удалось сгенерировать alias', 'error'); }
|
||
});
|
||
},
|
||
|
||
generateShortAlias: function (button) {
|
||
var self = this;
|
||
var input = this.field('document_short_alias');
|
||
if (!input) { return; }
|
||
var body = new FormData();
|
||
body.append('id', button.getAttribute('data-document-id') || '0');
|
||
body.append('_csrf', this.csrf());
|
||
fetch(button.getAttribute('data-short-generate'), {
|
||
method: 'POST', body: body,
|
||
headers: { 'Accept': 'application/json', 'X-CSRF-Token': this.csrf() },
|
||
credentials: 'same-origin'
|
||
})
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
var data = payload.data || {};
|
||
if (data.alias) {
|
||
input.value = data.alias;
|
||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||
} else if (payload.message && window.Adminx && Adminx.toast) {
|
||
Adminx.toast(payload.message, 'error');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
if (window.Adminx && Adminx.toast) { Adminx.toast('Не удалось сгенерировать короткий алиас', 'error'); }
|
||
});
|
||
},
|
||
|
||
updateSeo: function () {
|
||
if (!this.form) { return; }
|
||
var title = (this.field('document_title') || {}).value || '';
|
||
var desc = (this.field('document_meta_description') || {}).value || '';
|
||
var alias = (this.field('document_alias') || {}).value || '';
|
||
this.setSeoCount('title', title.length);
|
||
this.setSeoCount('description', desc.length);
|
||
var u = document.querySelector('[data-seo-snippet-url]'); if (u) { u.textContent = '/' + alias; }
|
||
var t = document.querySelector('[data-seo-snippet-title]'); if (t) { t.textContent = title || 'Заголовок документа'; }
|
||
var d = document.querySelector('[data-seo-snippet-desc]'); if (d) { d.textContent = desc || 'Описание появится после заполнения meta description.'; }
|
||
},
|
||
|
||
setSeoCount: function (key, len) {
|
||
var el = document.querySelector('[data-seo-count="' + key + '"]');
|
||
if (!el) { return; }
|
||
var max = parseInt(el.getAttribute('data-seo-max'), 10) || 0;
|
||
el.textContent = len + '/' + max;
|
||
el.classList.toggle('is-over', max > 0 && len > max);
|
||
},
|
||
|
||
openMediaPicker: function (target) {
|
||
var self = this;
|
||
var input = target ? target.querySelector('[data-document-media-url]') : null;
|
||
var list = target ? target.closest('[data-document-media-list], [data-document-media-single]') : null;
|
||
var fallbackDir = list ? (list.getAttribute('data-picker-dir') || '/uploads') : '/uploads';
|
||
Adminx.MediaPicker.open({
|
||
type: this.mediaPickerType(target),
|
||
dir: this.mediaDirectory(input ? input.value : '', fallbackDir),
|
||
title: 'Выбрать файл',
|
||
description: 'Путь будет записан в поле документа.',
|
||
onPick: function (file) { self.applyPickedMedia(target, file); }
|
||
});
|
||
},
|
||
|
||
mediaDirectory: function (value, fallback) {
|
||
var path = String(value || '').trim();
|
||
var safeFallback = String(fallback || '/uploads').trim() || '/uploads';
|
||
if (!path) { return safeFallback; }
|
||
try {
|
||
path = new URL(path, window.location.origin).pathname;
|
||
path = decodeURIComponent(path);
|
||
} catch (e) {
|
||
path = path.split(/[?#]/, 1)[0];
|
||
}
|
||
path = path.replace(/\\/g, '/').replace(/\/+/g, '/');
|
||
if (path !== '/uploads' && path.indexOf('/uploads/') !== 0) { return safeFallback; }
|
||
var slash = path.lastIndexOf('/');
|
||
return slash > 0 ? (path.slice(0, slash) || '/uploads') : safeFallback;
|
||
},
|
||
|
||
applyPickedMedia: function (target, file) {
|
||
var input = target ? target.querySelector('[data-document-media-url]') : null;
|
||
if (input && file && file.url) { input.value = file.url; input.dispatchEvent(new Event('input', { bubbles: true })); }
|
||
},
|
||
|
||
mediaPickerType: function (target) {
|
||
var input = target ? target.querySelector('[data-document-picker-type]') : null;
|
||
var type = input ? input.getAttribute('data-document-picker-type') : '';
|
||
if (!type && target && target.closest('[data-media-accept]')) {
|
||
type = target.closest('[data-media-accept]').getAttribute('data-media-accept') || '';
|
||
}
|
||
return type === 'file' || type === 'all' ? type : 'image';
|
||
},
|
||
|
||
addMediaRow: function (list) {
|
||
if (!list) { return; }
|
||
var fieldId = list.getAttribute('data-field-id') || '';
|
||
var type = list.getAttribute('data-field-type') || 'image_multi';
|
||
var box = list.querySelector('[data-document-media-items]');
|
||
if (!box) { return; }
|
||
box.insertAdjacentHTML('beforeend', this.mediaRowHtml(fieldId, type, box.querySelectorAll('[data-document-media-row]').length));
|
||
this.updateMediaListEmpty(list);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
openMediaUpload: function (list) {
|
||
var input = list ? list.querySelector('[data-document-media-files]') : null;
|
||
if (input) { input.click(); }
|
||
},
|
||
|
||
uploadMediaFiles: function (container, files) {
|
||
if (!container || !files || !files.length) { return; }
|
||
var data = new FormData();
|
||
data.append('_csrf', this.csrf());
|
||
data.append('dir', container.getAttribute('data-upload-dir') || '/uploads');
|
||
data.append('field_id', container.getAttribute('data-field-id') || '0');
|
||
data.append('document_id', (this.field('id') || {}).value || '0');
|
||
data.append('media_draft_token', (this.field('media_draft_token') || {}).value || '');
|
||
Array.prototype.forEach.call(files, function (file) { data.append('files[]', file); });
|
||
var self = this;
|
||
this.ajax(container.getAttribute('data-upload-url') || (this.base() + '/media/upload'), data, function (payload) {
|
||
var uploaded = ((payload.data || {}).files) || [];
|
||
if (container.matches('[data-document-media-single]')) {
|
||
self.applyUploadedSingle(container, uploaded[0] || null);
|
||
} else {
|
||
self.appendMediaFiles(container, uploaded);
|
||
}
|
||
Adminx.Toast.show(payload.message || 'Файлы загружены', 'success');
|
||
});
|
||
},
|
||
|
||
applyUploadedSingle: function (container, file) {
|
||
var input = container ? container.querySelector('[data-document-media-url]') : null;
|
||
var url = this.mediaFileUrl(file);
|
||
if (!input || !url) { return; }
|
||
input.value = url;
|
||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||
},
|
||
|
||
refreshMediaDraft: function (token, documentId) {
|
||
var tokenInput = this.field('media_draft_token');
|
||
if (tokenInput) { tokenInput.value = token; }
|
||
documentId = parseInt(documentId, 10) || 0;
|
||
document.querySelectorAll('[data-document-media-list], [data-document-media-single]').forEach(function (container) {
|
||
var fieldId = container.getAttribute('data-field-id') || '0';
|
||
container.setAttribute('data-upload-dir', '/uploads/.drafts/' + token + '/' + fieldId);
|
||
var target = container.getAttribute('data-target-dir') || '';
|
||
if (documentId && target.indexOf('{ID}') !== -1) {
|
||
target = target.replace(/\{ID\}/g, String(documentId));
|
||
container.setAttribute('data-target-dir', target);
|
||
container.setAttribute('data-picker-dir', target);
|
||
var targetLabel = container.querySelector('[data-document-media-target]');
|
||
if (targetLabel) { targetLabel.textContent = target; }
|
||
}
|
||
});
|
||
},
|
||
|
||
importMediaFolder: function (list) {
|
||
if (!list) { return; }
|
||
var self = this;
|
||
Adminx.MediaPicker.open({
|
||
type: list.getAttribute('data-media-accept') || 'image',
|
||
dir: list.getAttribute('data-picker-dir') || '/uploads',
|
||
title: 'Добавить файлы из папки',
|
||
description: 'Откройте нужную папку и подтвердите выбор. В поле добавятся все подходящие файлы из неё.',
|
||
folderLabel: 'Добавить из этой папки',
|
||
onFolder: function (dir) { self.loadMediaFolder(list, dir); }
|
||
});
|
||
},
|
||
|
||
loadMediaFolder: function (list, dir) {
|
||
var self = this;
|
||
var params = new URLSearchParams();
|
||
params.set('dir', dir || list.getAttribute('data-picker-dir') || '/uploads');
|
||
params.set('type', list.getAttribute('data-media-accept') || 'image');
|
||
params.set('limit', 1000);
|
||
fetch(this.base() + '/media/folder-files?' + params.toString(), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
var files = ((payload.data || {}).files) || [];
|
||
self.appendMediaFiles(list, files);
|
||
Adminx.Toast.show(files.length ? ('Добавлено из папки: ' + files.length) : 'В папке нет подходящих файлов', files.length ? 'success' : 'info');
|
||
})
|
||
.catch(function (payload) { Adminx.Toast.show((payload && payload.message) || 'Не удалось прочитать папку', 'error'); });
|
||
},
|
||
|
||
appendMediaFiles: function (list, files) {
|
||
if (!list || !files || !files.length) { return; }
|
||
var seen = {};
|
||
list.querySelectorAll('[data-document-media-url]').forEach(function (input) {
|
||
var value = String(input.value || '').trim();
|
||
if (value) { seen[value] = true; }
|
||
});
|
||
var added = 0;
|
||
var self = this;
|
||
Array.prototype.forEach.call(files, function (file) {
|
||
var url = self.mediaFileUrl(file);
|
||
if (!url || seen[url]) { return; }
|
||
var row = self.createMediaRow(list);
|
||
var input = row ? row.querySelector('[data-media-key="url"]') : null;
|
||
if (!input) { return; }
|
||
input.value = url;
|
||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||
var name = row.querySelector('[data-media-key="name"]');
|
||
if (name && !name.value) { name.value = file.name || url.split('/').pop(); }
|
||
seen[url] = true;
|
||
added++;
|
||
});
|
||
this.updateMediaListEmpty(list);
|
||
if (!added && files.length) { Adminx.Toast.show('Эти файлы уже есть в поле', 'info'); }
|
||
},
|
||
|
||
createMediaRow: function (list) {
|
||
this.addMediaRow(list);
|
||
var rows = list.querySelectorAll('[data-document-media-row]');
|
||
return rows.length ? rows[rows.length - 1] : null;
|
||
},
|
||
|
||
mediaFileUrl: function (file) {
|
||
if (!file) { return ''; }
|
||
return String(file.url || file.path || '').trim();
|
||
},
|
||
|
||
removeMediaRow: function (row) {
|
||
if (!row) { return; }
|
||
var list = row.closest('[data-document-media-list]');
|
||
row.remove();
|
||
this.reindexMediaList(list);
|
||
this.updateMediaListEmpty(list);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
moveMediaRow: function (row, direction) {
|
||
if (!row) { return; }
|
||
var list = row.closest('[data-document-media-list]');
|
||
var moved = false;
|
||
if (direction < 0 && row.previousElementSibling) {
|
||
row.parentNode.insertBefore(row, row.previousElementSibling);
|
||
moved = true;
|
||
}
|
||
if (direction > 0 && row.nextElementSibling) {
|
||
row.parentNode.insertBefore(row.nextElementSibling, row);
|
||
moved = true;
|
||
}
|
||
this.reindexMediaList(list);
|
||
if (moved) { this.setDirty(true); }
|
||
},
|
||
|
||
clearMediaRows: function (list) {
|
||
if (!list) { return; }
|
||
var self = this;
|
||
var clear = function () {
|
||
var box = list.querySelector('[data-document-media-items]');
|
||
if (box) { box.innerHTML = ''; }
|
||
self.updateMediaListEmpty(list);
|
||
self.setDirty(true);
|
||
};
|
||
if (Adminx.Confirm) {
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Очистить поле?',
|
||
message: 'Все элементы этого поля будут удалены из документа после сохранения.',
|
||
confirmLabel: 'Очистить',
|
||
onConfirm: clear
|
||
});
|
||
return;
|
||
}
|
||
if (confirm('Очистить все элементы поля?')) { clear(); }
|
||
},
|
||
|
||
updateMediaPreview: function (input) {
|
||
var key = input.getAttribute('data-media-key');
|
||
if (key && key !== 'url') { return; }
|
||
var scope = input.closest('[data-document-media-row]') || input.closest('.documents-single-media-field');
|
||
if (!scope) { return; }
|
||
var url = String(input.value || '').trim();
|
||
var isDoc = scope.classList.contains('documents-media-row-doc_files') || (scope.getAttribute('data-media-accept') === 'all');
|
||
|
||
var preview = scope.querySelector('.documents-media-thumb, .documents-media-preview');
|
||
if (preview) {
|
||
if (url && /\.(jpe?g|png|gif|webp|bmp|svg)(\?.*)?$/i.test(url)) {
|
||
preview.innerHTML = '<img src="' + this.esc(url) + '" alt="">';
|
||
} else {
|
||
preview.innerHTML = '<span><i class="ti ti-' + (isDoc ? 'file' : 'photo') + '"></i></span>';
|
||
}
|
||
}
|
||
|
||
var path = scope.querySelector('[data-media-path]');
|
||
if (path) {
|
||
path.innerHTML = url
|
||
? '<code>' + this.esc(url) + '</code>'
|
||
: '<span class="documents-media-path-empty">' + (isDoc ? 'Файл не выбран' : 'Изображение не выбрано') + '</span>';
|
||
}
|
||
},
|
||
|
||
updateMediaListEmpty: function (list) {
|
||
if (!list) { return; }
|
||
var empty = list.querySelector('[data-document-media-empty]');
|
||
var count = list.querySelectorAll('[data-document-media-row]').length;
|
||
if (empty) { empty.hidden = count > 0; }
|
||
this.reindexMediaList(list);
|
||
},
|
||
|
||
reindexMediaLists: function () {
|
||
var self = this;
|
||
if (!this.form) { return; }
|
||
this.form.querySelectorAll('[data-document-media-list]').forEach(function (list) { self.reindexMediaList(list); });
|
||
},
|
||
|
||
reindexMediaList: function (list) {
|
||
if (!list) { return; }
|
||
var fieldId = list.getAttribute('data-field-id') || '';
|
||
list.querySelectorAll('[data-document-media-row]').forEach(function (row, index) {
|
||
row.querySelectorAll('[data-media-key]').forEach(function (input) {
|
||
input.name = 'fields[' + fieldId + '][items][' + index + '][' + input.getAttribute('data-media-key') + ']';
|
||
});
|
||
});
|
||
},
|
||
|
||
// ---- drag-and-drop сортировка строк медиа/значений ----
|
||
rowDragStart: function (e) {
|
||
var handle = e.target.closest('[data-doc-drag]');
|
||
if (!handle) { return; }
|
||
var row = handle.closest('[data-document-media-row], [data-document-value-row]');
|
||
if (!row) { return; }
|
||
this.dragRow = row;
|
||
this.dragContainer = row.parentNode;
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
try { e.dataTransfer.setData('text/plain', ''); } catch (err) {}
|
||
setTimeout(function () { row.classList.add('documents-row-dragging'); }, 0);
|
||
},
|
||
|
||
rowDragOver: function (e) {
|
||
if (!this.dragRow) { return; }
|
||
var over = e.target.closest('[data-document-media-row], [data-document-value-row]');
|
||
if (!over || over === this.dragRow || over.parentNode !== this.dragContainer) { return; }
|
||
e.preventDefault();
|
||
var rect = over.getBoundingClientRect();
|
||
var after;
|
||
if (Math.abs(e.clientY - (rect.top + rect.height / 2)) > rect.height / 2) {
|
||
after = e.clientY > rect.top + rect.height / 2;
|
||
} else {
|
||
after = e.clientX > rect.left + rect.width / 2;
|
||
}
|
||
this.dragContainer.insertBefore(this.dragRow, after ? over.nextSibling : over);
|
||
},
|
||
|
||
rowDragEnd: function () {
|
||
if (!this.dragRow) { return; }
|
||
this.dragRow.classList.remove('documents-row-dragging');
|
||
var mediaList = this.dragRow.closest('[data-document-media-list]');
|
||
var valueList = this.dragRow.closest('[data-document-value-list]');
|
||
this.dragRow = null;
|
||
this.dragContainer = null;
|
||
if (mediaList) { this.reindexMediaList(mediaList); }
|
||
if (valueList) { this.reindexValueList(valueList); }
|
||
this.setDirty(true);
|
||
},
|
||
|
||
mediaRowHtml: function (fieldId, type, index) {
|
||
var prefix = 'fields[' + this.esc(fieldId) + '][items][' + this.esc(index) + ']';
|
||
var isImage = type !== 'doc_files';
|
||
var thumb = '<button class="documents-media-thumb" type="button" data-document-media-pick aria-label="Выбрать файл"><span><i class="ti ti-' + (isImage ? 'photo' : 'file') + '"></i></span></button>';
|
||
var path = '<div class="documents-media-path" data-media-path><span class="documents-media-path-empty">' + (isImage ? 'Изображение не выбрано' : 'Файл не выбран') + '</span></div>';
|
||
var actions = '<div class="documents-media-card-actions">'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm documents-drag-handle" type="button" data-doc-drag draggable="true" data-tooltip="Перетащить" aria-label="Перетащить"><i class="ti ti-grip-vertical"></i></button>'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-up data-tooltip="Выше" aria-label="Выше"><i class="ti ti-arrow-up"></i></button>'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-down data-tooltip="Ниже" aria-label="Ниже"><i class="ti ti-arrow-down"></i></button>'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm documents-media-remove" type="button" data-document-media-remove data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>'
|
||
+ '</div>';
|
||
var picker = function (name, placeholder, icon) {
|
||
var pickerType = name === 'link' || type === 'doc_files' ? 'all' : 'image';
|
||
return '<div class="documents-picker-input"><input class="input mono" type="text" name="' + prefix + '[' + name + ']" data-media-key="' + name + '" value="" placeholder="' + placeholder + '" data-document-media-url data-document-picker-type="' + pickerType + '"><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Выбрать файл" aria-label="Выбрать файл"><i class="ti ti-' + icon + '"></i></button></div>';
|
||
};
|
||
var linkField = function () {
|
||
return '<div class="documents-picker-input documents-picker-input-link"><input class="input mono" type="text" name="' + prefix + '[link]" data-media-key="link" value="" placeholder="Ссылка или документ" data-document-media-url data-document-picker-type="all">'
|
||
+ '<button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Выбрать файл" aria-label="Выбрать файл"><i class="ti ti-paperclip"></i></button>'
|
||
+ '<button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-doc-pick data-tooltip="Выбрать документ" aria-label="Выбрать документ"><i class="ti ti-file-search"></i></button></div>';
|
||
};
|
||
var fields;
|
||
if (type === 'doc_files') {
|
||
fields = '<input class="input" type="text" name="' + prefix + '[name]" data-media-key="name" value="" placeholder="Название файла">'
|
||
+ '<textarea class="textarea" name="' + prefix + '[description]" data-media-key="description" rows="2" placeholder="Описание"></textarea>'
|
||
+ picker('url', '/uploads/file.pdf', 'paperclip');
|
||
} else if (type === 'image_mega') {
|
||
fields = picker('url', '/uploads/image.jpg', 'photo-plus')
|
||
+ '<input class="input" type="text" name="' + prefix + '[title]" data-media-key="title" value="" placeholder="Заголовок изображения">'
|
||
+ '<textarea class="textarea" name="' + prefix + '[description]" data-media-key="description" rows="2" placeholder="Описание"></textarea>'
|
||
+ linkField();
|
||
} else {
|
||
fields = picker('url', '/uploads/image.jpg', 'photo-plus')
|
||
+ '<textarea class="textarea" name="' + prefix + '[description]" data-media-key="description" rows="2" placeholder="Описание"></textarea>';
|
||
}
|
||
return '<div class="documents-media-card documents-media-row documents-media-row-' + this.esc(type) + '" data-document-media-row>'
|
||
+ thumb
|
||
+ '<div class="documents-media-card-body">'
|
||
+ path
|
||
+ '<div class="documents-media-row-fields">' + fields + '</div>'
|
||
+ actions
|
||
+ '</div></div>';
|
||
},
|
||
|
||
addValueRow: function (list) {
|
||
if (!list) { return; }
|
||
var fieldId = list.getAttribute('data-field-id') || '';
|
||
var kind = list.getAttribute('data-list-kind') || 'list_pair';
|
||
var type = list.getAttribute('data-field-type') || '';
|
||
var dimensionUnit = list.getAttribute('data-dimension-unit') || 'см';
|
||
var weightUnit = list.getAttribute('data-weight-unit') || 'кг';
|
||
var box = list.querySelector('[data-document-value-items]');
|
||
if (!box) { return; }
|
||
box.insertAdjacentHTML('beforeend', this.valueRowHtml(fieldId, kind, type, box.querySelectorAll('[data-document-value-row]').length, dimensionUnit, weightUnit));
|
||
this.updateValueListEmpty(list);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
removeValueRow: function (row) {
|
||
if (!row) { return; }
|
||
var list = row.closest('[data-document-value-list]');
|
||
row.remove();
|
||
this.reindexValueList(list);
|
||
this.updateValueListEmpty(list);
|
||
this.setDirty(true);
|
||
},
|
||
|
||
moveValueRow: function (row, direction) {
|
||
if (!row) { return; }
|
||
var list = row.closest('[data-document-value-list]');
|
||
var moved = false;
|
||
if (direction < 0 && row.previousElementSibling) {
|
||
row.parentNode.insertBefore(row, row.previousElementSibling);
|
||
moved = true;
|
||
}
|
||
if (direction > 0 && row.nextElementSibling) {
|
||
row.parentNode.insertBefore(row.nextElementSibling, row);
|
||
moved = true;
|
||
}
|
||
this.reindexValueList(list);
|
||
if (moved) { this.setDirty(true); }
|
||
},
|
||
|
||
clearValueRows: function (list) {
|
||
if (!list) { return; }
|
||
var self = this;
|
||
var clear = function () {
|
||
var box = list.querySelector('[data-document-value-items]');
|
||
if (box) { box.innerHTML = ''; }
|
||
self.updateValueListEmpty(list);
|
||
self.setDirty(true);
|
||
};
|
||
if (Adminx.Confirm) {
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Очистить поле?',
|
||
message: 'Все строки этого поля будут удалены из документа после сохранения.',
|
||
confirmLabel: 'Очистить',
|
||
onConfirm: clear
|
||
});
|
||
return;
|
||
}
|
||
if (confirm('Очистить все строки поля?')) { clear(); }
|
||
},
|
||
|
||
updateValueListEmpty: function (list) {
|
||
if (!list) { return; }
|
||
var empty = list.querySelector('[data-document-value-empty]');
|
||
var count = list.querySelectorAll('[data-document-value-row]').length;
|
||
if (empty) { empty.hidden = count > 0; }
|
||
this.reindexValueList(list);
|
||
},
|
||
|
||
reindexValueLists: function () {
|
||
var self = this;
|
||
if (!this.form) { return; }
|
||
this.form.querySelectorAll('[data-document-value-list]').forEach(function (list) { self.reindexValueList(list); });
|
||
},
|
||
|
||
reindexValueList: function (list) {
|
||
if (!list) { return; }
|
||
var fieldId = list.getAttribute('data-field-id') || '';
|
||
list.querySelectorAll('[data-document-value-row]').forEach(function (row, index) {
|
||
row.querySelectorAll('[data-value-key]').forEach(function (input) {
|
||
input.name = 'fields[' + fieldId + '][items][' + index + '][' + input.getAttribute('data-value-key') + ']';
|
||
});
|
||
});
|
||
},
|
||
|
||
valueRowHtml: function (fieldId, kind, type, index, dimensionUnit, weightUnit) {
|
||
var prefix = 'fields[' + this.esc(fieldId) + '][items][' + this.esc(index) + ']';
|
||
var tools = '<div class="documents-value-row-tools">'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm documents-drag-handle" type="button" data-doc-drag draggable="true" data-tooltip="Перетащить" aria-label="Перетащить"><i class="ti ti-grip-vertical"></i></button>'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-up data-tooltip="Выше" aria-label="Выше"><i class="ti ti-arrow-up"></i></button>'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-down data-tooltip="Ниже" aria-label="Ниже"><i class="ti ti-arrow-down"></i></button>'
|
||
+ '</div>';
|
||
var remove = '<button class="btn btn-ghost btn-icon btn-sm documents-value-remove" type="button" data-document-value-remove data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>';
|
||
var input = function (name, placeholder) {
|
||
return '<input class="input" type="text" name="' + prefix + '[' + name + ']" data-value-key="' + name + '" value="" placeholder="' + placeholder + '">';
|
||
};
|
||
var fields = '';
|
||
if (kind === 'list_single') {
|
||
fields = input('value', 'Значение');
|
||
} else if (kind === 'packages') {
|
||
fields = input('length', 'Длина, ' + (dimensionUnit || 'см'))
|
||
+ input('width', 'Ширина, ' + (dimensionUnit || 'см'))
|
||
+ input('height', 'Высота, ' + (dimensionUnit || 'см'))
|
||
+ input('weight', 'Вес, ' + (weightUnit || 'кг'));
|
||
} else if (kind === 'list_triple') {
|
||
fields = input('param', type === 'multi_list_triple' ? 'Колонка 1' : 'Заголовок')
|
||
+ input('value', type === 'multi_list_triple' ? 'Колонка 2' : 'Значение')
|
||
+ input('value2', type === 'multi_list_triple' ? 'Колонка 3' : 'Дополнительно');
|
||
} else {
|
||
fields = input('param', type === 'multi_links' ? 'Название' : (type === 'teasers' ? 'Заголовок' : 'Параметр'))
|
||
+ input('value', type === 'multi_links' ? 'URL' : (type === 'teasers' ? 'ID тизера / ссылка' : 'Значение'));
|
||
}
|
||
return '<div class="documents-value-row documents-value-row-' + this.esc(kind) + '" data-document-value-row>'
|
||
+ tools + '<div class="documents-value-row-fields">' + fields + '</div>' + remove + '</div>';
|
||
},
|
||
|
||
openRelationPicker: function (target, opts) {
|
||
var self = this;
|
||
opts = opts || {};
|
||
var subtitle = opts.insert === 'linkAlias'
|
||
? 'В поле-ссылку будет записан адрес документа.'
|
||
: 'В поле будет записан ID документа.';
|
||
var overlay = document.createElement('div');
|
||
overlay.className = 'overlay documents-picker-overlay';
|
||
overlay.innerHTML = '<div class="modal picker-modal documents-relation-picker" role="dialog" aria-modal="true">'
|
||
+ '<div class="modal-header"><span class="dialog-icon info"><i class="ti ti-file-search"></i></span><div style="flex:1"><h3>Выбрать документ</h3><p class="text-secondary" style="margin-top:4px">' + subtitle + '</p></div><button class="modal-close" type="button" data-relation-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div>'
|
||
+ '<div class="modal-body"><div class="input-wrap documents-relation-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="ID, название или alias" data-relation-search></div><div class="documents-picker-status" data-relation-status>Загрузка...</div><div class="documents-relation-list" data-relation-list></div></div>'
|
||
+ '<div class="modal-footer"><div class="mf-left documents-picker-count" data-relation-count></div><button class="btn btn-ghost" type="button" data-relation-close>Закрыть</button></div>'
|
||
+ '</div>';
|
||
document.body.appendChild(overlay);
|
||
requestAnimationFrame(function () { overlay.classList.add('show'); });
|
||
this.bindRelationPicker(overlay, target, opts);
|
||
},
|
||
|
||
bindRelationPicker: function (overlay, target, opts) {
|
||
var self = this;
|
||
opts = opts || {};
|
||
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); }, 160);
|
||
};
|
||
var render = function (items) {
|
||
list.innerHTML = '';
|
||
(items || []).forEach(function (item) {
|
||
list.insertAdjacentHTML('beforeend', '<button class="documents-relation-item" type="button" data-relation-id="' + self.esc(item.id) + '" data-relation-alias="' + self.esc(item.alias || '') + '"><span class="documents-relation-id">#' + self.esc(item.id) + '</span><span><b>' + self.esc(item.title || 'Без названия') + '</b><small>' + self.esc(item.alias || '') + '</small></span><em>' + self.esc(item.rubric_title || ('Рубрика #' + item.rubric_id)) + '</em></button>');
|
||
});
|
||
status.hidden = items && items.length > 0;
|
||
status.textContent = search.value.trim() ? 'Ничего не найдено' : 'Начните вводить название или ID';
|
||
count.textContent = items && items.length ? 'Документов: ' + items.length : '';
|
||
};
|
||
var rubricEl = target ? (target.querySelector && target.querySelector('[data-document-picker-rubric]')) : null;
|
||
if (!rubricEl && target && target.matches && target.matches('[data-document-picker-rubric]')) { rubricEl = target; }
|
||
var rubric = rubricEl ? (rubricEl.getAttribute('data-document-picker-rubric') || '') : '';
|
||
var load = function () {
|
||
var params = new URLSearchParams();
|
||
params.set('q', search.value.trim());
|
||
params.set('limit', 20);
|
||
if (rubric) { params.set('rubric_id', rubric); }
|
||
status.textContent = 'Загрузка...';
|
||
status.hidden = false;
|
||
fetch(self.base() + '/documents/picker?' + params.toString(), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(self.json)
|
||
.then(function (payload) { render((payload.data || {}).items || []); })
|
||
.catch(function () { list.innerHTML = ''; status.textContent = 'Не удалось загрузить документы'; status.hidden = false; });
|
||
};
|
||
var timer = null;
|
||
var onKey = function (e) { if (e.key === 'Escape') { close(); } };
|
||
overlay.addEventListener('click', function (e) {
|
||
if (e.target === overlay || e.target.closest('[data-relation-close]')) { close(); return; }
|
||
var item = e.target.closest('[data-relation-id]');
|
||
if (item) {
|
||
if (opts.insert === 'linkAlias' && opts.input) {
|
||
var alias = (item.getAttribute('data-relation-alias') || '').replace(/^\/+/, '');
|
||
opts.input.value = alias ? '/' + alias : '';
|
||
opts.input.dispatchEvent(new Event('input', { bubbles: true }));
|
||
} else {
|
||
var isMultiple = !!(target && target.querySelector('[data-document-relation-list]'));
|
||
self.applyPickedRelation(
|
||
target,
|
||
item.getAttribute('data-relation-id'),
|
||
item.getAttribute('data-relation-alias'),
|
||
item.querySelector('b') ? item.querySelector('b').textContent : ''
|
||
);
|
||
if (isMultiple) { item.classList.add('is-selected'); return; }
|
||
}
|
||
close();
|
||
}
|
||
});
|
||
search.addEventListener('input', function () {
|
||
clearTimeout(timer);
|
||
timer = setTimeout(load, 220);
|
||
});
|
||
document.addEventListener('keydown', onKey);
|
||
load();
|
||
search.focus();
|
||
},
|
||
|
||
applyPickedRelation: function (target, id, alias, title) {
|
||
var multiple = target ? target.querySelector('[data-document-relation-list]') : null;
|
||
if (multiple) {
|
||
this.appendPickedRelation(multiple, id, title);
|
||
return;
|
||
}
|
||
var input = target ? (target.querySelector('[data-document-relation-id]') || target.querySelector('[data-document-parent-id]')) : null;
|
||
if (input) { input.value = id || ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
|
||
// Одиночная связь «Документ из рубрики»: показываем заголовок выбранного документа, а не ID.
|
||
var single = target ? target.querySelector('[data-document-relation-single]') : null;
|
||
if (single) { this.fillRelationSingle(single, id, title); }
|
||
// Выбор родительского документа перестраивает ЧПУ по иерархии: <алиас-родителя>/<leaf>.
|
||
if (input && input.hasAttribute('data-document-parent-id')) {
|
||
this.rebuildAliasFromParent(alias);
|
||
}
|
||
},
|
||
|
||
fillRelationSingle: function (single, id, title) {
|
||
if (!single) { return; }
|
||
id = String(id || '').replace(/[^0-9]/g, '');
|
||
var titleEl = single.querySelector('[data-relation-title]');
|
||
var idEl = single.querySelector('[data-relation-idlabel]');
|
||
var clear = single.querySelector('[data-document-relation-clear]');
|
||
if (titleEl) { titleEl.textContent = id ? (title || ('Документ #' + id)) : 'Документ не выбран'; }
|
||
if (idEl) { idEl.textContent = id ? '#' + id : 'нажмите, чтобы выбрать'; }
|
||
single.classList.toggle('is-empty', !id);
|
||
if (clear) { clear.hidden = !id; }
|
||
},
|
||
|
||
appendPickedRelation: function (list, id, title) {
|
||
id = String(id || '').replace(/[^0-9]/g, '');
|
||
if (!list || !id || list.querySelector('[data-document-relation-token][data-relation-id="' + id + '"]')) { return; }
|
||
var fieldId = list.getAttribute('data-field-id') || '';
|
||
var tokens = list.querySelector('[data-document-relation-tokens]');
|
||
if (!tokens) { return; }
|
||
tokens.insertAdjacentHTML('beforeend', '<span class="documents-relation-token" data-document-relation-token data-relation-id="' + this.esc(id) + '">'
|
||
+ '<span><b>' + this.esc(title || ('Документ #' + id)) + '</b><small>#' + this.esc(id) + '</small></span>'
|
||
+ '<input type="hidden" name="fields[' + this.esc(fieldId) + '][document_ids][]" value="' + this.esc(id) + '">'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-relation-remove aria-label="Убрать документ" data-tooltip="Убрать"><i class="ti ti-x"></i></button></span>');
|
||
var empty = list.querySelector('[data-document-relation-empty]');
|
||
if (empty) { empty.hidden = true; }
|
||
this.setDirty(true);
|
||
},
|
||
|
||
removePickedRelation: function (token) {
|
||
if (!token) { return; }
|
||
var list = token.closest('[data-document-relation-list]');
|
||
token.remove();
|
||
var empty = list ? list.querySelector('[data-document-relation-empty]') : null;
|
||
if (empty) { empty.hidden = !!list.querySelector('[data-document-relation-token]'); }
|
||
this.setDirty(true);
|
||
},
|
||
|
||
rebuildAliasFromParent: function (parentAlias) {
|
||
var aliasInput = this.field('document_alias');
|
||
if (!aliasInput) { return; }
|
||
var current = String(aliasInput.value || '').replace(/^\/+|\/+$/g, '');
|
||
var leaf = current.indexOf('/') >= 0 ? current.split('/').pop() : current;
|
||
if (!leaf) {
|
||
var title = this.field('document_title');
|
||
leaf = title ? this.slugify(title.value) : '';
|
||
}
|
||
var parent = String(parentAlias || '').replace(/^\/+|\/+$/g, '');
|
||
aliasInput.value = parent ? (parent + '/' + leaf) : leaf;
|
||
this.aliasTouched = true;
|
||
this.checkAlias(false);
|
||
this.updateSeo();
|
||
},
|
||
|
||
openRemarks: function () {
|
||
if (!this.form) { return; }
|
||
var id = parseInt(this.field('id').value, 10) || 0;
|
||
if (!id) { return; }
|
||
this.currentRemarkDocumentId = id;
|
||
var subtitle = document.querySelector('[data-document-remarks-subtitle]');
|
||
if (subtitle) { subtitle.textContent = '#' + id + ' · ' + (this.field('document_title') ? this.field('document_title').value : 'Документ'); }
|
||
if (Adminx.Drawer) { Adminx.Drawer.open('documentRemarksDrawer'); }
|
||
this.refreshRemarks();
|
||
},
|
||
|
||
refreshRemarks: function () {
|
||
if (!this.currentRemarkDocumentId) { return; }
|
||
var self = this;
|
||
var list = document.querySelector('[data-document-remark-list]');
|
||
if (list) { list.innerHTML = '<div class="empty-state">Загрузка...</div>'; }
|
||
fetch(this.base() + '/documents/' + this.currentRemarkDocumentId + '/remarks', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) { self.renderRemarks(((payload.data || {}).remarks) || []); })
|
||
.catch(function () { Adminx.Toast.show('Не удалось загрузить заметки', 'error'); });
|
||
},
|
||
|
||
renderRemarks: function (items) {
|
||
var list = document.querySelector('[data-document-remark-list]');
|
||
var self = this;
|
||
if (!list) { return; }
|
||
var badge = document.querySelector('[data-document-remarks-badge]');
|
||
if (badge) { badge.textContent = items.length; }
|
||
if (!items.length) { list.innerHTML = '<div class="empty-state">Заметок пока нет.</div>'; return; }
|
||
list.innerHTML = items.map(function (item) {
|
||
return '<article class="card documents-remark">'
|
||
+ '<div class="documents-remark-head"><div><b>' + self.esc(item.title || 'Без заголовка') + '</b><small>' + self.esc(item.published_label || '-') + (item.email ? ' · ' + self.esc(item.email) : '') + '</small></div><button class="btn btn-ghost btn-icon btn-sm documents-action-danger" type="button" data-document-remark-delete="' + self.esc(item.id) + '" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button></div>'
|
||
+ '<p>' + self.esc(item.text || '') + '</p></article>';
|
||
}).join('');
|
||
},
|
||
|
||
submitRemark: function (form) {
|
||
if (!this.currentRemarkDocumentId || !form) { return; }
|
||
var data = new FormData(form);
|
||
data.append('_csrf', this.csrf());
|
||
var self = this;
|
||
this.ajax(this.base() + '/documents/' + this.currentRemarkDocumentId + '/remarks', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Заметка добавлена', 'success');
|
||
form.reset();
|
||
self.refreshRemarks();
|
||
});
|
||
},
|
||
|
||
deleteRemark: function (id) {
|
||
id = parseInt(id, 10) || 0;
|
||
if (!id || !this.currentRemarkDocumentId) { return; }
|
||
var self = this;
|
||
Adminx.Confirm.open({kind: 'danger', title: 'Удалить заметку?', message: 'Заметка будет удалена без восстановления.', confirmLabel: 'Удалить', onConfirm: function () {
|
||
var data = new FormData(); data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/' + self.currentRemarkDocumentId + '/remarks/' + id + '/delete', data, function (payload) { Adminx.Toast.show(payload.message || 'Заметка удалена', 'success'); self.refreshRemarks(); });
|
||
}});
|
||
},
|
||
|
||
openAliases: function () {
|
||
if (!this.form) { return; }
|
||
var id = parseInt(this.field('id').value, 10) || 0;
|
||
if (!id) { return; }
|
||
this.currentAliasDocumentId = id;
|
||
this.resetAliasForm();
|
||
var subtitle = document.querySelector('[data-document-aliases-subtitle]');
|
||
if (subtitle) { subtitle.textContent = '#' + id + ' · ' + (this.field('document_title') ? this.field('document_title').value : 'Документ'); }
|
||
if (Adminx.Drawer) { Adminx.Drawer.open('documentAliasesDrawer'); }
|
||
this.refreshAliases();
|
||
},
|
||
|
||
refreshAliases: function () {
|
||
if (!this.currentAliasDocumentId) { return; }
|
||
var self = this;
|
||
var list = document.querySelector('[data-document-alias-list]');
|
||
if (list) { list.innerHTML = '<div class="empty-state">Загрузка...</div>'; }
|
||
Adminx.Loader.show();
|
||
fetch(this.base() + '/documents/' + this.currentAliasDocumentId + '/aliases', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) {
|
||
var data = payload.data || {};
|
||
var target = document.querySelector('[data-document-alias-target]');
|
||
var targetAlias = ((data.document || {}).alias) || '';
|
||
if (target) { target.textContent = '/' + targetAlias; target.href = '/' + targetAlias; }
|
||
self.renderAliases(data.aliases || []);
|
||
})
|
||
.catch(function () { Adminx.Toast.show('Не удалось загрузить историю URL', 'error'); })
|
||
.finally(function () { Adminx.Loader.hide(); });
|
||
},
|
||
|
||
renderAliases: function (items) {
|
||
var list = document.querySelector('[data-document-alias-list]');
|
||
var self = this;
|
||
if (!list) { return; }
|
||
var count = document.querySelector('[data-document-aliases-count]');
|
||
if (count) { count.textContent = items.length; }
|
||
if (!items.length) {
|
||
list.innerHTML = '<div class="empty-state">Старых URL пока нет.</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = items.map(function (item) {
|
||
return '<article class="list-row documents-alias-row" data-alias="' + self.esc(item.alias) + '" data-header="' + self.esc(item.header) + '">'
|
||
+ '<span class="icon-tile documents-alias-icon"><i class="ti ti-route"></i></span>'
|
||
+ '<div class="documents-alias-main"><a class="mono" href="/' + self.esc(item.alias) + '" target="_blank" rel="noopener">/' + self.esc(item.alias) + '</a><small>' + self.esc(item.changed_label || '-') + (item.author_name ? ' · ' + self.esc(item.author_name) : '') + '</small></div>'
|
||
+ '<span class="badge badge-cyan">' + self.esc(item.header || 301) + '</span>'
|
||
+ '<div class="cluster documents-alias-actions">'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-alias-edit="' + self.esc(item.id) + '" data-tooltip="Изменить" aria-label="Изменить"><i class="ti ti-pencil"></i></button>'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm documents-action-danger" type="button" data-document-alias-delete="' + self.esc(item.id) + '" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>'
|
||
+ '</div></article>';
|
||
}).join('');
|
||
},
|
||
|
||
editAlias: function (button) {
|
||
var row = button ? button.closest('.documents-alias-row') : null;
|
||
var form = document.querySelector('[data-document-alias-form]');
|
||
if (!row || !form) { return; }
|
||
form.elements.id.value = button.getAttribute('data-document-alias-edit') || '0';
|
||
form.elements.alias.value = row.getAttribute('data-alias') || '';
|
||
form.elements.header.value = row.getAttribute('data-header') || '301';
|
||
var title = form.querySelector('[data-document-alias-form-title]');
|
||
if (title) { title.textContent = 'Редактирование редиректа'; }
|
||
form.elements.alias.focus();
|
||
},
|
||
|
||
resetAliasForm: function () {
|
||
var form = document.querySelector('[data-document-alias-form]');
|
||
if (!form) { return; }
|
||
form.reset();
|
||
form.elements.id.value = '0';
|
||
var title = form.querySelector('[data-document-alias-form-title]');
|
||
if (title) { title.textContent = 'Новый редирект'; }
|
||
},
|
||
|
||
submitAlias: function (form) {
|
||
if (!this.currentAliasDocumentId || !form) { return; }
|
||
var historyId = parseInt(form.elements.id.value, 10) || 0;
|
||
var data = new FormData(form);
|
||
data.append('_csrf', this.csrf());
|
||
var self = this;
|
||
this.ajax(this.base() + '/documents/' + this.currentAliasDocumentId + '/aliases' + (historyId ? '/' + historyId : ''), data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Редирект сохранён', 'success');
|
||
self.resetAliasForm();
|
||
self.refreshAliases();
|
||
});
|
||
},
|
||
|
||
deleteAlias: function (id) {
|
||
id = parseInt(id, 10) || 0;
|
||
if (!id || !this.currentAliasDocumentId) { return; }
|
||
var self = this;
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Удалить редирект?',
|
||
message: 'Старый URL перестанет перенаправлять на документ.',
|
||
confirmLabel: 'Удалить',
|
||
onConfirm: function () {
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/' + self.currentAliasDocumentId + '/aliases/' + id + '/delete', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Редирект удалён', 'success');
|
||
self.resetAliasForm();
|
||
self.refreshAliases();
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
openRevisions: function () {
|
||
if (!this.form) { return; }
|
||
var id = parseInt(this.field('id').value, 10) || 0;
|
||
if (!id) { return; }
|
||
this.currentRevisionDocumentId = id;
|
||
this.currentRevisionId = 0;
|
||
var title = document.getElementById('documentRevisionsDrawerTitle');
|
||
var subtitle = document.getElementById('documentRevisionsDrawerSubtitle');
|
||
var list = document.querySelector('[data-document-revisions-list]');
|
||
var count = document.querySelector('[data-document-revisions-count]');
|
||
var revisionTitle = document.querySelector('[data-document-revision-title]');
|
||
var revisionMeta = document.querySelector('[data-document-revision-meta]');
|
||
var fields = document.querySelector('[data-document-revision-fields]');
|
||
var restore = document.querySelector('[data-document-revision-restore]');
|
||
var restoreLabel = document.querySelector('[data-document-revision-restore-label]');
|
||
var remove = document.querySelector('[data-document-revision-delete]');
|
||
var publicPreview = document.querySelector('[data-document-revision-preview]');
|
||
var clear = document.querySelector('[data-document-revisions-clear]');
|
||
if (title) { title.textContent = 'Ревизии документа #' + id; }
|
||
if (subtitle) { subtitle.textContent = this.field('document_title') ? this.field('document_title').value : 'История значений полей'; }
|
||
if (list) { list.innerHTML = '<div class="empty-state">Загрузка...</div>'; }
|
||
if (count) { count.textContent = 'Загрузка...'; }
|
||
if (revisionTitle) { revisionTitle.textContent = 'Выберите ревизию'; }
|
||
if (revisionMeta) { revisionMeta.textContent = 'Содержимое снимка появится после выбора ревизии.'; }
|
||
if (fields) { fields.innerHTML = ''; }
|
||
if (restore) { restore.disabled = true; }
|
||
if (restoreLabel) { restoreLabel.textContent = 'Восстановить'; }
|
||
if (remove) { remove.disabled = true; remove.removeAttribute('data-document-revision-delete'); }
|
||
if (publicPreview) { publicPreview.hidden = true; publicPreview.href = '#'; }
|
||
if (clear) { clear.disabled = true; }
|
||
if (Adminx.Drawer) { Adminx.Drawer.open('documentRevisionsDrawer'); }
|
||
this.refreshRevisions();
|
||
},
|
||
|
||
refreshRevisions: function () {
|
||
if (!this.currentRevisionDocumentId) { return; }
|
||
var self = this;
|
||
Adminx.Loader.show();
|
||
fetch(this.base() + '/documents/' + this.currentRevisionDocumentId + '/revisions', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) { self.renderRevisions(((payload.data || {}).revisions) || []); })
|
||
.catch(function () { Adminx.Toast.show('Не удалось загрузить ревизии', 'error'); })
|
||
.finally(function () { Adminx.Loader.hide(); });
|
||
},
|
||
|
||
renderRevisions: function (items) {
|
||
var list = document.querySelector('[data-document-revisions-list]');
|
||
var count = document.querySelector('[data-document-revisions-count]');
|
||
var clear = document.querySelector('[data-document-revisions-clear]');
|
||
var badge = document.querySelector('[data-document-revisions-badge]');
|
||
var self = this;
|
||
if (count) { count.textContent = items.length ? (items.length + ' снимков') : 'История пока пустая'; }
|
||
if (badge) { badge.textContent = items.length; }
|
||
if (clear) { clear.disabled = !items.length; }
|
||
if (!list) { return; }
|
||
if (!items.length) {
|
||
list.innerHTML = '<div class="empty-state">Ревизий пока нет. Первый снимок появится после сохранения документа.</div>';
|
||
this.showRevisionEmpty();
|
||
return;
|
||
}
|
||
list.innerHTML = items.map(function (item) {
|
||
return '<div class="list-row documents-revision-row" data-document-revision-open="' + self.esc(item.id) + '">'
|
||
+ '<span class="icon-tile documents-revision-icon"><i class="ti ti-history"></i></span>'
|
||
+ '<div class="documents-revision-row-main">'
|
||
+ '<b>Ревизия #' + self.esc(item.id) + '</b><span class="badge ' + (item.format_version > 1 ? 'badge-green' : 'badge-gray') + '">' + self.esc(item.format_label || 'Только поля') + '</span><span class="badge badge-blue">' + self.esc(item.fields_count || 0) + ' полей</span>'
|
||
+ '<div class="text-muted text-xs">' + self.esc(item.created_label || '-') + (item.author_name ? ' · ' + self.esc(item.author_name) : '') + (item.size_label ? ' · ' + self.esc(item.size_label) : '') + '</div>'
|
||
+ '</div>'
|
||
+ '<button class="btn btn-ghost btn-icon btn-sm documents-revision-delete" type="button" data-document-revision-delete="' + self.esc(item.id) + '" data-tooltip="Удалить ревизию" aria-label="Удалить ревизию"><i class="ti ti-trash"></i></button>'
|
||
+ '</div>';
|
||
}).join('');
|
||
this.loadRevision(items[0].id);
|
||
},
|
||
|
||
showRevisionEmpty: function () {
|
||
var title = document.querySelector('[data-document-revision-title]');
|
||
var meta = document.querySelector('[data-document-revision-meta]');
|
||
var fields = document.querySelector('[data-document-revision-fields]');
|
||
var restore = document.querySelector('[data-document-revision-restore]');
|
||
var restoreLabel = document.querySelector('[data-document-revision-restore-label]');
|
||
var remove = document.querySelector('[data-document-revision-delete]');
|
||
var publicPreview = document.querySelector('[data-document-revision-preview]');
|
||
if (title) { title.textContent = 'Выберите ревизию'; }
|
||
if (meta) { meta.textContent = 'Содержимое снимка появится после выбора ревизии.'; }
|
||
if (fields) { fields.innerHTML = ''; }
|
||
if (restore) { restore.disabled = true; }
|
||
if (restoreLabel) { restoreLabel.textContent = 'Восстановить'; }
|
||
if (remove) { remove.disabled = true; remove.removeAttribute('data-document-revision-delete'); }
|
||
if (publicPreview) { publicPreview.hidden = true; publicPreview.href = '#'; }
|
||
},
|
||
|
||
loadRevision: function (id) {
|
||
id = parseInt(id, 10) || 0;
|
||
if (!id) { return; }
|
||
var self = this;
|
||
Adminx.Loader.show();
|
||
fetch(this.base() + '/documents/revisions/' + id, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
|
||
.then(this.json)
|
||
.then(function (payload) { self.showRevision(payload.data || {}); })
|
||
.catch(function () { Adminx.Toast.show('Не удалось загрузить ревизию', 'error'); })
|
||
.finally(function () { Adminx.Loader.hide(); });
|
||
},
|
||
|
||
showRevision: function (item) {
|
||
var self = this;
|
||
this.currentRevisionId = parseInt(item.id, 10) || 0;
|
||
document.querySelectorAll('[data-document-revision-open]').forEach(function (row) {
|
||
row.classList.toggle('is-active', row.getAttribute('data-document-revision-open') === String(item.id));
|
||
});
|
||
var title = document.querySelector('[data-document-revision-title]');
|
||
var meta = document.querySelector('[data-document-revision-meta]');
|
||
var fields = document.querySelector('[data-document-revision-fields]');
|
||
var restore = document.querySelector('[data-document-revision-restore]');
|
||
var remove = document.querySelector('[data-document-revision-delete]');
|
||
var publicPreview = document.querySelector('[data-document-revision-preview]');
|
||
if (title) { title.textContent = 'Ревизия #' + (item.id || ''); }
|
||
if (meta) { meta.textContent = (item.created_label || '-') + (item.author_name ? ' · ' + item.author_name : '') + (item.size_label ? ' · ' + item.size_label : ''); }
|
||
if (fields) {
|
||
var preview = item.preview || [];
|
||
var documentPreview = item.document_preview || [];
|
||
var systemHtml = documentPreview.length ? '<section class="documents-revision-system"><div class="documents-revision-subhead"><i class="ti ti-settings"></i><b>Основные настройки</b><span>' + self.esc(documentPreview.length) + '</span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="document" checked><span>Все</span></label></div>' + documentPreview.map(function (field) {
|
||
return '<article class="documents-revision-field documents-revision-system-field"><div><label class="documents-revision-check" aria-label="Восстановить ' + self.esc(field.title || field.key) + '"><input type="checkbox" value="' + self.esc(field.key || '') + '" data-document-revision-select="document" checked></label><span class="documents-revision-field-name"><b>' + self.esc(field.title || field.key) + '</b><small>' + self.esc(field.key || '') + '</small></span></div><pre>' + self.esc(field.value_preview || '') + '</pre></article>';
|
||
}).join('') + '</section>' : '';
|
||
var fieldsHtml = preview.length ? '<section class="documents-revision-content"><div class="documents-revision-subhead"><i class="ti ti-forms"></i><b>Поля рубрики</b><span>' + self.esc(preview.length) + '</span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="field" checked><span>Все</span></label></div>' + preview.map(function (field) {
|
||
return '<article class="documents-revision-field">'
|
||
+ '<div><label class="documents-revision-check" aria-label="Восстановить ' + self.esc(field.title || ('Поле #' + field.field_id)) + '"><input type="checkbox" value="' + self.esc(field.field_id) + '" data-document-revision-select="field" checked></label><span class="documents-revision-field-name"><b>' + self.esc(field.title || ('Поле #' + field.field_id)) + '</b><small>#' + self.esc(field.field_id) + (field.type ? ' · ' + self.esc(field.type) : '') + '</small></span><span>' + self.esc(field.size_label || '') + '</span></div>'
|
||
+ '<pre>' + self.esc(field.value_preview || '') + '</pre>'
|
||
+ '</article>';
|
||
}).join('') + '</section>' : '<div class="empty-state">В снимке нет значений полей.</div>';
|
||
fields.innerHTML = systemHtml + fieldsHtml;
|
||
}
|
||
this.updateRevisionSelection();
|
||
if (publicPreview) {
|
||
publicPreview.hidden = !item.preview_url;
|
||
publicPreview.href = item.preview_url || '#';
|
||
}
|
||
if (remove) {
|
||
remove.disabled = !this.currentRevisionId;
|
||
if (this.currentRevisionId) { remove.setAttribute('data-document-revision-delete', String(this.currentRevisionId)); }
|
||
}
|
||
},
|
||
|
||
toggleRevisionGroup: function (group, checked) {
|
||
document.querySelectorAll('[data-document-revision-select="' + group + '"]').forEach(function (input) {
|
||
input.checked = !!checked;
|
||
});
|
||
this.updateRevisionSelection();
|
||
},
|
||
|
||
revisionSelection: function () {
|
||
var all = Array.prototype.slice.call(document.querySelectorAll('[data-document-revision-select]'));
|
||
var selected = all.filter(function (input) { return input.checked; });
|
||
return {
|
||
all: all.length > 0 && selected.length === all.length,
|
||
count: selected.length,
|
||
total: all.length,
|
||
documentKeys: selected.filter(function (input) { return input.getAttribute('data-document-revision-select') === 'document'; }).map(function (input) { return input.value; }),
|
||
fieldIds: selected.filter(function (input) { return input.getAttribute('data-document-revision-select') === 'field'; }).map(function (input) { return parseInt(input.value, 10) || 0; }).filter(Boolean)
|
||
};
|
||
},
|
||
|
||
updateRevisionSelection: function () {
|
||
var selection = this.revisionSelection();
|
||
document.querySelectorAll('[data-document-revision-group]').forEach(function (group) {
|
||
var type = group.getAttribute('data-document-revision-group');
|
||
var items = Array.prototype.slice.call(document.querySelectorAll('[data-document-revision-select="' + type + '"]'));
|
||
var checked = items.filter(function (input) { return input.checked; }).length;
|
||
group.checked = items.length > 0 && checked === items.length;
|
||
group.indeterminate = checked > 0 && checked < items.length;
|
||
});
|
||
var restore = document.querySelector('[data-document-revision-restore]');
|
||
var label = document.querySelector('[data-document-revision-restore-label]');
|
||
if (restore) { restore.disabled = !this.currentRevisionId || selection.count === 0; }
|
||
if (label) { label.textContent = selection.all ? 'Восстановить всё' : 'Восстановить выбранное · ' + selection.count; }
|
||
},
|
||
|
||
deleteRevision: function (id) {
|
||
id = parseInt(id || this.currentRevisionId, 10) || 0;
|
||
if (!id) { return; }
|
||
var self = this;
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Удалить ревизию?',
|
||
message: 'Снимок будет удалён без восстановления.',
|
||
confirmLabel: 'Удалить',
|
||
onConfirm: function () {
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/revisions/' + id + '/delete', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Ревизия удалена', 'success');
|
||
self.currentRevisionId = 0;
|
||
self.refreshRevisions();
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
clearRevisions: function () {
|
||
if (!this.currentRevisionDocumentId) { return; }
|
||
var self = this;
|
||
Adminx.Confirm.open({
|
||
kind: 'danger',
|
||
title: 'Удалить все ревизии?',
|
||
message: 'Будет очищена вся история снимков этого документа.',
|
||
confirmLabel: 'Удалить все',
|
||
onConfirm: function () {
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
self.ajax(self.base() + '/documents/' + self.currentRevisionDocumentId + '/revisions/delete', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Ревизии удалены', 'success');
|
||
self.currentRevisionId = 0;
|
||
self.refreshRevisions();
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
restoreRevision: function () {
|
||
if (!this.currentRevisionId) { return; }
|
||
var self = this;
|
||
var id = this.currentRevisionId;
|
||
var selection = this.revisionSelection();
|
||
if (!selection.count) { Adminx.Toast.show('Выберите данные для восстановления', 'warning'); return; }
|
||
Adminx.Confirm.open({
|
||
kind: 'warning',
|
||
title: selection.all ? 'Восстановить ревизию?' : 'Восстановить выбранные данные?',
|
||
message: 'Текущее состояние документа будет сохранено отдельной ревизией. Затем восстановится ' + (selection.all ? 'весь выбранный снимок.' : selection.count + ' отмеченных элементов.'),
|
||
confirmLabel: 'Восстановить',
|
||
onConfirm: function () {
|
||
var data = new FormData();
|
||
data.append('_csrf', self.csrf());
|
||
if (!selection.all) {
|
||
data.append('selection_mode', 'selected');
|
||
selection.documentKeys.forEach(function (key) { data.append('document_keys[]', key); });
|
||
selection.fieldIds.forEach(function (fieldId) { data.append('field_ids[]', String(fieldId)); });
|
||
}
|
||
self.ajax(self.base() + '/documents/revisions/' + id + '/restore', data, function (payload) {
|
||
Adminx.Toast.show(payload.message || 'Документ восстановлен', 'success');
|
||
var redirect = payload.redirect || (self.base() + '/documents/' + self.currentRevisionDocumentId + '/edit');
|
||
window.location.href = redirect;
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
esc: function (value) {
|
||
return String(value == null ? '' : value).replace(/[&<>"']/g, function (ch) {
|
||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch];
|
||
});
|
||
},
|
||
|
||
setSubtitle: function () {}
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', function () { Adminx.Documents.init(); });
|
||
})(window, document);
|